Forex & Futures

Course 5: Forex & Futures Algorithmic Trading

Course Overview

Duration Modules Exercises Level
~40 hours 14 + Capstone ~90 Intermediate

Course Description

Trade the world's largest markets. Master forex and futures, understand leverage, implement global macro strategies, and build systems for 24-hour operation.

Prerequisites

  • Course 0: Python for Finance - Python programming, pandas, data analysis

What You'll Learn

  • Navigate the forex market (currency pairs, pips, lots, trading sessions)
  • Understand futures contracts (expiration, rollover, basis, contango/backwardation)
  • Master leverage and margin management
  • Connect to OANDA API for data and trading
  • Implement forex-specific strategies (trend following, carry trade, news trading)
  • Build commodity trading systems (gold, oil, agricultural)
  • Manage risk with proper position sizing and gap risk handling
  • Deploy 24-hour automated trading systems

Course Structure

Part 1: Market Fundamentals

Understand the unique characteristics of forex and futures markets

Module Title Key Topics
1 Forex Market Basics Currency pairs, market structure, sessions, pips & lots
2 Futures Market Basics Contract specs, expiration, basis, rollover
3 Leverage & Margin Leverage mechanics, margin requirements, risk management
4 Data & Tools OANDA API, data sources, real-time feeds

Part 2: Analysis & Strategies

Learn analysis techniques and trading strategies specific to forex/futures

Module Title Key Topics
5 Technical Analysis Chart patterns, forex indicators, multi-timeframe
6 Fundamental Analysis Economic indicators, central banks, calendar
7 Commodity Trading Gold, oil, agricultural, commodity currencies
8 Trading Strategies Trend following, range, carry trade, news trading

Part 3: Risk & Execution

Manage risk and execute trades in leveraged markets

Module Title Key Topics
9 Risk Management Forex/futures risk, portfolio risk, gap risk
10 Backtesting Spread costs, leveraged backtesting, tick data
11 Live Trading Brokers, OANDA execution, MetaTrader, 24h operation

Part 4: Advanced Topics

Master advanced strategies and build production systems

Module Title Key Topics
12 Advanced Strategies Currency indices, spreads, options, seasonality
13 Automation & Monitoring 24/7 scheduling, alerts, performance tracking
14 Trading Psychology & Journaling Psychology, journals, performance review

Capstone Project

Build a 24-Hour Forex/Futures Trading System with multi-currency monitoring, economic calendar integration, leveraged risk management, and automated journaling.


Why Forex & Futures?

The World's Largest Markets

  • Forex: $7.5 trillion daily volume (largest financial market)
  • Futures: Standardized contracts for commodities, indices, currencies
  • 24-hour trading: Opportunities around the clock
  • High liquidity: Tight spreads on major pairs

Unique Characteristics

Feature Forex Futures Stocks
Trading Hours 24/5 Near 24/5 Market hours only
Leverage 50:1 to 500:1 10:1 to 20:1 2:1 to 4:1
Minimum Capital Very low Moderate Moderate
Settlement T+2 or rolling Physical/Cash T+2
Short Selling Native (sell currency) Easy Requires borrowing

Key Concepts to Master

  1. Leverage - Amplifies both gains and losses
  2. Pips & Lots - Forex-specific terminology
  3. Rollover/Swap - Interest on held positions
  4. Basis & Contango - Futures pricing dynamics
  5. Economic Calendar - News drives forex markets

Environment Setup

# Install required packages (uncomment to run)
# !pip install oandapyV20 MetaTrader5 ta yfinance pandas numpy matplotlib
# Core imports for the course
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta

# Set display options
pd.set_option('display.max_columns', 20)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')

print("Environment ready!")
print(f"pandas version: {pd.__version__}")
print(f"numpy version: {np.__version__}")

Forex Market Preview

# Major currency pairs
major_pairs = {
    'EUR/USD': 'Euro vs US Dollar',
    'GBP/USD': 'British Pound vs US Dollar',
    'USD/JPY': 'US Dollar vs Japanese Yen',
    'USD/CHF': 'US Dollar vs Swiss Franc',
    'AUD/USD': 'Australian Dollar vs US Dollar',
    'USD/CAD': 'US Dollar vs Canadian Dollar',
    'NZD/USD': 'New Zealand Dollar vs US Dollar'
}

print("Major Currency Pairs")
print("=" * 50)
for pair, description in major_pairs.items():
    print(f"{pair:12} - {description}")
# Trading sessions (UTC times)
sessions = {
    'Sydney': ('21:00', '06:00', 'AUD, NZD'),
    'Tokyo': ('00:00', '09:00', 'JPY pairs'),
    'London': ('08:00', '17:00', 'EUR, GBP'),
    'New York': ('13:00', '22:00', 'USD pairs')
}

print("\nForex Trading Sessions (UTC)")
print("=" * 50)
print(f"{'Session':12} {'Open':8} {'Close':8} {'Active Pairs'}")
print("-" * 50)
for session, (open_time, close_time, pairs) in sessions.items():
    print(f"{session:12} {open_time:8} {close_time:8} {pairs}")
# Pip value calculation example
def calculate_pip_value(pair: str, lot_size: float = 1.0, account_currency: str = 'USD') -> float:
    """
    Calculate pip value for a forex pair.
    
    Args:
        pair: Currency pair (e.g., 'EUR/USD')
        lot_size: Position size in lots (1 lot = 100,000 units)
        account_currency: Account denomination
        
    Returns:
        Pip value in account currency
    """
    # Standard lot = 100,000 units
    units = lot_size * 100_000
    
    # For pairs ending in USD, 1 pip = $10 per standard lot
    if pair.endswith('/USD'):
        pip_value = units * 0.0001
    # For JPY pairs, pip is 0.01
    elif 'JPY' in pair:
        pip_value = units * 0.01 / 150  # Approximate USD/JPY rate
    else:
        pip_value = units * 0.0001
    
    return pip_value

# Example calculations
print("\nPip Values (1 Standard Lot = 100,000 units)")
print("=" * 50)
for pair in ['EUR/USD', 'GBP/USD', 'USD/JPY']:
    pip_val = calculate_pip_value(pair, lot_size=1.0)
    print(f"{pair}: ${pip_val:.2f} per pip")

Futures Market Preview

# Popular futures contracts
futures_contracts = {
    'ES': {'name': 'E-mini S&P 500', 'exchange': 'CME', 'tick_size': 0.25, 'tick_value': 12.50},
    'NQ': {'name': 'E-mini Nasdaq 100', 'exchange': 'CME', 'tick_size': 0.25, 'tick_value': 5.00},
    'CL': {'name': 'Crude Oil', 'exchange': 'NYMEX', 'tick_size': 0.01, 'tick_value': 10.00},
    'GC': {'name': 'Gold', 'exchange': 'COMEX', 'tick_size': 0.10, 'tick_value': 10.00},
    '6E': {'name': 'Euro FX', 'exchange': 'CME', 'tick_size': 0.00005, 'tick_value': 6.25},
    'ZB': {'name': '30-Year Treasury Bond', 'exchange': 'CBOT', 'tick_size': 1/32, 'tick_value': 31.25}
}

print("Popular Futures Contracts")
print("=" * 70)
print(f"{'Symbol':8} {'Name':25} {'Exchange':10} {'Tick Size':12} {'Tick Value'}")
print("-" * 70)
for symbol, info in futures_contracts.items():
    tick_str = f"{info['tick_size']:.5f}".rstrip('0').rstrip('.')
    print(f"{symbol:8} {info['name']:25} {info['exchange']:10} {tick_str:12} ${info['tick_value']:.2f}")
# Contract month codes
month_codes = {
    'F': 'January', 'G': 'February', 'H': 'March', 'J': 'April',
    'K': 'May', 'M': 'June', 'N': 'July', 'Q': 'August',
    'U': 'September', 'V': 'October', 'X': 'November', 'Z': 'December'
}

print("\nFutures Contract Month Codes")
print("=" * 30)
for code, month in month_codes.items():
    print(f"{code} = {month}")
# Basis calculation example
def calculate_basis(spot_price: float, futures_price: float) -> dict:
    """
    Calculate futures basis and market condition.
    
    Args:
        spot_price: Current spot price
        futures_price: Futures contract price
        
    Returns:
        Dictionary with basis and market condition
    """
    basis = futures_price - spot_price
    basis_pct = (basis / spot_price) * 100
    
    if basis > 0:
        condition = 'Contango'
    elif basis < 0:
        condition = 'Backwardation'
    else:
        condition = 'Flat'
    
    return {
        'basis': basis,
        'basis_pct': basis_pct,
        'condition': condition
    }

# Example: Gold futures
spot_gold = 2050.00
futures_gold = 2065.50

result = calculate_basis(spot_gold, futures_gold)
print("\nGold Futures Basis Analysis")
print("=" * 40)
print(f"Spot Price:    ${spot_gold:.2f}")
print(f"Futures Price: ${futures_gold:.2f}")
print(f"Basis:         ${result['basis']:.2f} ({result['basis_pct']:.2f}%)")
print(f"Market:        {result['condition']}")

Leverage Preview

# Leverage impact visualization
def simulate_leveraged_returns(
    price_change_pct: float,
    leverage_levels: list = [1, 10, 50, 100]
) -> pd.DataFrame:
    """
    Simulate returns at different leverage levels.
    """
    results = []
    for leverage in leverage_levels:
        leveraged_return = price_change_pct * leverage
        results.append({
            'Leverage': f'{leverage}:1',
            'Price Change': f'{price_change_pct:.1f}%',
            'Account Return': f'{leveraged_return:.1f}%'
        })
    return pd.DataFrame(results)

# Show impact of 1% price move
print("Impact of 1% Price Move at Different Leverage Levels")
print("=" * 55)
df = simulate_leveraged_returns(1.0)
print(df.to_string(index=False))

print("\nImpact of -2% Price Move at Different Leverage Levels")
print("=" * 55)
df = simulate_leveraged_returns(-2.0)
print(df.to_string(index=False))
# Visualize leverage impact
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Price changes from -5% to +5%
price_changes = np.linspace(-5, 5, 100)
leverage_levels = [1, 10, 50, 100]

# Plot returns
colors = ['green', 'blue', 'orange', 'red']
for leverage, color in zip(leverage_levels, colors):
    returns = price_changes * leverage
    axes[0].plot(price_changes, returns, label=f'{leverage}:1', color=color, linewidth=2)

axes[0].axhline(y=0, color='black', linestyle='-', linewidth=0.5)
axes[0].axvline(x=0, color='black', linestyle='-', linewidth=0.5)
axes[0].axhline(y=-100, color='red', linestyle='--', linewidth=1, alpha=0.7)
axes[0].set_xlabel('Price Change (%)')
axes[0].set_ylabel('Account Return (%)')
axes[0].set_title('Leverage Impact on Returns')
axes[0].legend()
axes[0].set_ylim(-150, 150)
axes[0].grid(True, alpha=0.3)

# Plot margin call threshold
leverage_range = np.arange(1, 101)
margin_call_threshold = 100 / leverage_range  # % move to wipe out account

axes[1].plot(leverage_range, margin_call_threshold, color='red', linewidth=2)
axes[1].fill_between(leverage_range, margin_call_threshold, 0, alpha=0.3, color='red')
axes[1].set_xlabel('Leverage Ratio')
axes[1].set_ylabel('Adverse Move to Wipe Out Account (%)')
axes[1].set_title('Margin Call Threshold by Leverage')
axes[1].grid(True, alpha=0.3)

# Add annotations
axes[1].annotate('50:1 = 2% move', xy=(50, 2), xytext=(60, 10),
                 arrowprops=dict(arrowstyle='->', color='black'),
                 fontsize=10)

plt.tight_layout()
plt.show()

OANDA API Preview

Throughout this course, we'll use the OANDA API for forex data and trading. Here's a preview of how it works.

# Mock OANDA-style data for demonstration
# In real use, you'll connect to OANDA's API

def generate_forex_data(
    pair: str = 'EUR_USD',
    days: int = 30,
    start_price: float = 1.0850
) -> pd.DataFrame:
    """
    Generate mock forex OHLC data.
    
    Args:
        pair: Currency pair
        days: Number of days
        start_price: Starting price
        
    Returns:
        DataFrame with OHLC data
    """
    np.random.seed(42)
    dates = pd.date_range(end=pd.Timestamp.today(), periods=days, freq='D')
    
    # Generate price path
    returns = np.random.normal(0, 0.005, days)
    close_prices = start_price * np.cumprod(1 + returns)
    
    # Generate OHLC from close
    daily_volatility = 0.003
    high_prices = close_prices * (1 + np.abs(np.random.normal(0, daily_volatility, days)))
    low_prices = close_prices * (1 - np.abs(np.random.normal(0, daily_volatility, days)))
    open_prices = np.roll(close_prices, 1)
    open_prices[0] = start_price
    
    df = pd.DataFrame({
        'open': open_prices,
        'high': high_prices,
        'low': low_prices,
        'close': close_prices,
        'volume': np.random.randint(10000, 50000, days)
    }, index=dates)
    
    return df

# Generate sample data
eur_usd = generate_forex_data('EUR_USD', days=60)
print("EUR/USD Sample Data")
print("=" * 60)
print(eur_usd.tail(10).round(5))
# Visualize forex data
fig, axes = plt.subplots(2, 1, figsize=(12, 8), height_ratios=[3, 1])

# Candlestick-style visualization using bars
ax1 = axes[0]
colors = ['green' if row['close'] >= row['open'] else 'red' 
          for _, row in eur_usd.iterrows()]

# Plot high-low lines
for i, (idx, row) in enumerate(eur_usd.iterrows()):
    ax1.plot([i, i], [row['low'], row['high']], color=colors[i], linewidth=1)
    ax1.plot([i, i], [row['open'], row['close']], color=colors[i], linewidth=4)

ax1.set_ylabel('Price')
ax1.set_title('EUR/USD Price Chart')
ax1.grid(True, alpha=0.3)

# Set x-axis labels
tick_positions = range(0, len(eur_usd), 10)
tick_labels = [eur_usd.index[i].strftime('%m/%d') for i in tick_positions]
ax1.set_xticks(tick_positions)
ax1.set_xticklabels(tick_labels)

# Volume
ax2 = axes[1]
ax2.bar(range(len(eur_usd)), eur_usd['volume'], color=colors, alpha=0.7)
ax2.set_ylabel('Volume')
ax2.set_xlabel('Date')
ax2.set_xticks(tick_positions)
ax2.set_xticklabels(tick_labels)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Course Projects Overview

Each module ends with a hands-on project:

Module Project
1 Forex Market Analyzer
2 Futures Data Handler
3 Leverage Risk Calculator
4 Forex Data System
5 Technical Analysis Suite
6 Fundamental Dashboard
7 Commodity Trading System
8 Multi-Strategy Forex System
9 Risk Management System
10 Forex/Futures Backtester
11 Live Trading System
12 Advanced Strategy Toolkit
13 Production Monitoring System
14 Automated Trading Journal
Capstone 24-Hour Forex/Futures Trading System

Getting Started

Recommended Setup

  1. OANDA Demo Account
  2. Sign up at OANDA
  3. Create practice account for API access
  4. No deposit required for demo trading

  5. Install Required Packages bash pip install oandapyV20 MetaTrader5 ta yfinance pandas numpy matplotlib

  6. Start Learning

  7. Begin with Module 1: Forex Market Basics
  8. Complete all exercises (3 guided + 3 open-ended per module)
  9. Build each module project

Tips for Success

  • Start with demo accounts - Never trade real money while learning
  • Respect leverage - It's a double-edged sword
  • Understand costs - Spreads, swaps, and commissions add up
  • Master risk management - Position sizing is critical in leveraged markets
  • Practice 24h thinking - These markets never sleep

Key Takeaways

  • Forex is the largest financial market with $7.5 trillion daily volume
  • Futures provide standardized contracts for commodities, indices, and currencies
  • Leverage amplifies both gains and losses - handle with care
  • 24-hour markets require different operational approaches
  • Economic events drive forex markets more than other asset classes

Next: Module 1 - Forex Market Basics

Module 1: Forex Market Basics

Part 1: Market Fundamentals

Duration Exercises
~2.5 hours 6

Learning Objectives

By the end of this module, you will be able to:

  • Understand how currency pairs work (base vs quote currency)
  • Identify major, minor, and exotic currency pairs
  • Navigate the decentralized forex market structure
  • Understand trading sessions and their characteristics
  • Calculate pip values for different currency pairs
  • Work with lot sizes and position sizing
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Tuple, Optional

# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')

1.1 What is Forex?

The foreign exchange market (Forex or FX) is the global marketplace for trading currencies. It's the largest and most liquid financial market in the world, with a daily trading volume exceeding $7.5 trillion.

Currency Pairs Explained

In forex, currencies are always traded in pairs. When you trade EUR/USD, you're simultaneously: - Buying one currency (base currency) - Selling another currency (quote currency)

EUR/USD = 1.0850
 │   │      │
 │   │      └── Price: 1 EUR = 1.0850 USD
 │   └── Quote Currency (what you pay)
 └── Base Currency (what you buy)
# Understanding currency pairs
class CurrencyPair:
    """
    Represents a forex currency pair.
    
    Attributes:
        base: Base currency code
        quote: Quote currency code
        price: Current exchange rate
    """
    
    def __init__(self, base: str, quote: str, price: float):
        self.base = base
        self.quote = quote
        self.price = price
    
    @property
    def symbol(self) -> str:
        """Return the pair symbol."""
        return f"{self.base}/{self.quote}"
    
    def convert(self, amount: float, direction: str = 'base_to_quote') -> float:
        """
        Convert an amount between currencies.
        
        Args:
            amount: Amount to convert
            direction: 'base_to_quote' or 'quote_to_base'
            
        Returns:
            Converted amount
        """
        if direction == 'base_to_quote':
            return amount * self.price
        else:
            return amount / self.price
    
    def __repr__(self) -> str:
        return f"CurrencyPair({self.symbol} = {self.price})"


# Example usage
eur_usd = CurrencyPair('EUR', 'USD', 1.0850)
print(f"Pair: {eur_usd.symbol}")
print(f"Rate: {eur_usd.price}")
print(f"\n10,000 EUR = {eur_usd.convert(10000):.2f} USD")
print(f"10,000 USD = {eur_usd.convert(10000, 'quote_to_base'):.2f} EUR")

Major, Minor, and Exotic Pairs

Currency pairs are categorized based on trading volume and liquidity:

Category Description Examples Spread
Major USD paired with major economies EUR/USD, GBP/USD, USD/JPY Tightest (0.5-2 pips)
Minor Major currencies without USD EUR/GBP, EUR/JPY, GBP/JPY Moderate (2-5 pips)
Exotic Major + emerging market currency USD/TRY, EUR/ZAR, USD/MXN Widest (5-50+ pips)
# Currency pair classifications
MAJOR_PAIRS = {
    'EUR/USD': {'name': 'Euro', 'nickname': 'Fiber'},
    'GBP/USD': {'name': 'British Pound', 'nickname': 'Cable'},
    'USD/JPY': {'name': 'Japanese Yen', 'nickname': 'Gopher'},
    'USD/CHF': {'name': 'Swiss Franc', 'nickname': 'Swissie'},
    'AUD/USD': {'name': 'Australian Dollar', 'nickname': 'Aussie'},
    'USD/CAD': {'name': 'Canadian Dollar', 'nickname': 'Loonie'},
    'NZD/USD': {'name': 'New Zealand Dollar', 'nickname': 'Kiwi'}
}

MINOR_PAIRS = [
    'EUR/GBP', 'EUR/JPY', 'EUR/CHF', 'EUR/AUD', 'EUR/CAD', 'EUR/NZD',
    'GBP/JPY', 'GBP/CHF', 'GBP/AUD', 'GBP/CAD', 'GBP/NZD',
    'AUD/JPY', 'AUD/CHF', 'AUD/CAD', 'AUD/NZD',
    'CAD/JPY', 'CAD/CHF', 'CHF/JPY', 'NZD/JPY'
]

EXOTIC_PAIRS = [
    'USD/TRY', 'USD/ZAR', 'USD/MXN', 'USD/SGD', 'USD/HKD',
    'USD/SEK', 'USD/NOK', 'USD/DKK', 'USD/PLN', 'USD/HUF',
    'EUR/TRY', 'EUR/ZAR', 'EUR/NOK', 'EUR/SEK', 'EUR/PLN'
]

print("Major Pairs (USD-based, highest liquidity)")
print("=" * 55)
for pair, info in MAJOR_PAIRS.items():
    print(f"{pair:10} - {info['name']:25} ({info['nickname']})")

print(f"\nMinor Pairs (Cross pairs without USD): {len(MINOR_PAIRS)} pairs")
print(f"Exotic Pairs (Emerging market currencies): {len(EXOTIC_PAIRS)} pairs")
def classify_pair(pair: str) -> str:
    """
    Classify a currency pair as major, minor, or exotic.
    
    Args:
        pair: Currency pair symbol (e.g., 'EUR/USD')
        
    Returns:
        Classification string
    """
    pair = pair.upper().replace('_', '/')
    
    if pair in MAJOR_PAIRS:
        return 'Major'
    elif pair in MINOR_PAIRS:
        return 'Minor'
    elif pair in EXOTIC_PAIRS:
        return 'Exotic'
    else:
        # Check if it could be exotic based on currency codes
        exotic_currencies = ['TRY', 'ZAR', 'MXN', 'SGD', 'HKD', 'SEK', 'NOK', 
                           'DKK', 'PLN', 'HUF', 'CZK', 'RUB', 'BRL', 'INR']
        base, quote = pair.split('/')
        if base in exotic_currencies or quote in exotic_currencies:
            return 'Exotic'
        return 'Unknown'

# Test classification
test_pairs = ['EUR/USD', 'GBP/JPY', 'USD/TRY', 'AUD/NZD', 'EUR/ZAR']
print("Pair Classification")
print("=" * 30)
for pair in test_pairs:
    print(f"{pair:12} -> {classify_pair(pair)}")

1.2 Market Structure

Decentralized Market

Unlike stock exchanges, forex has no central exchange. It operates as an Over-The-Counter (OTC) market where trading happens electronically between participants worldwide.

Market Hierarchy

                    ┌─────────────────────┐
                    │   Interbank Market  │ ← Largest banks trade directly
                    │  (Tier 1 Liquidity) │
                    └──────────┬──────────┘
                               │
              ┌────────────────┼────────────────┐
              │                │                │
     ┌────────┴────────┐ ┌────┴────┐ ┌────────┴────────┐
     │ Prime Brokers   │ │  ECNs   │ │ Market Makers   │
     │ (Hedge Funds)   │ │         │ │ (Retail Flow)   │
     └────────┬────────┘ └────┬────┘ └────────┬────────┘
              │               │               │
              └───────────────┼───────────────┘
                              │
                    ┌─────────┴─────────┐
                    │  Retail Brokers   │
                    │  (Your Broker)    │
                    └─────────┬─────────┘
                              │
                    ┌─────────┴─────────┐
                    │   Retail Traders  │ ← You
                    └───────────────────┘
# Market participants and their characteristics
MARKET_PARTICIPANTS = {
    'Central Banks': {
        'role': 'Monetary policy, currency intervention',
        'volume': 'Massive but infrequent',
        'examples': ['Federal Reserve', 'ECB', 'BOJ', 'BOE']
    },
    'Commercial Banks': {
        'role': 'Interbank trading, customer orders',
        'volume': 'Very high (Tier 1 liquidity)',
        'examples': ['JP Morgan', 'Citi', 'Deutsche Bank', 'UBS']
    },
    'Investment Banks': {
        'role': 'Proprietary trading, market making',
        'volume': 'High',
        'examples': ['Goldman Sachs', 'Morgan Stanley']
    },
    'Hedge Funds': {
        'role': 'Speculative trading, arbitrage',
        'volume': 'High',
        'examples': ['Bridgewater', 'Citadel', 'Renaissance']
    },
    'Corporations': {
        'role': 'Hedging currency exposure',
        'volume': 'Moderate',
        'examples': ['Apple', 'Toyota', 'Nestlé']
    },
    'Retail Traders': {
        'role': 'Speculation',
        'volume': 'Low (5-10% of market)',
        'examples': ['Individual traders via brokers']
    }
}

print("Forex Market Participants")
print("=" * 70)
for participant, info in MARKET_PARTICIPANTS.items():
    print(f"\n{participant}")
    print(f"  Role: {info['role']}")
    print(f"  Volume: {info['volume']}")
    print(f"  Examples: {', '.join(info['examples'])}")
# Market share by participant type (approximate)
participant_shares = {
    'Commercial Banks': 42,
    'Investment Banks': 18,
    'Hedge Funds': 12,
    'Central Banks': 8,
    'Corporations': 8,
    'Retail Traders': 7,
    'Other': 5
}

# Visualize market share
fig, ax = plt.subplots(figsize=(10, 6))

colors = plt.cm.Set3(np.linspace(0, 1, len(participant_shares)))
wedges, texts, autotexts = ax.pie(
    participant_shares.values(),
    labels=participant_shares.keys(),
    autopct='%1.0f%%',
    colors=colors,
    explode=[0.02] * len(participant_shares),
    shadow=True
)

ax.set_title('Forex Market Share by Participant Type', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Exercise 1.1: Identify Currency Pair Types (Guided)

Complete the function to identify whether a currency pair is major, minor, or exotic.

Exercise
Solution 1.1
def identify_pair_type(pair: str) -> dict:
    """
    Identify the type and characteristics of a currency pair.

    Args:
        pair: Currency pair symbol (e.g., 'EUR/USD')

    Returns:
        Dictionary with pair type and characteristics
    """
    # Normalize the pair format
    pair = pair.upper().replace('_', '/')  # Convert to uppercase

    # Split into base and quote currencies
    base, quote = pair.split('/')  # Split by delimiter

    # Define major currencies
    major_currencies = ['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD']

    # Determine pair type
    has_usd = 'USD' in pair  # Check if USD is in the pair
    both_major = base in major_currencies and quote in major_currencies

    if has_usd and both_major:
        pair_type = 'Major'
        typical_spread = '0.5-2 pips'
    elif both_major and not has_usd:
        pair_type = 'Minor'
        typical_spread = '2-5 pips'
    else:
        pair_type = 'Exotic'
        typical_spread = '5-50+ pips'

    return {
        'pair': pair,
        'base': base,
        'quote': quote,
        'type': pair_type,
        'typical_spread': typical_spread
    }

1.3 Trading Sessions

The forex market operates 24 hours a day, 5 days a week. Trading activity rotates through four major sessions as the sun moves around the globe.

Session Times (UTC)

Session Open (UTC) Close (UTC) Key Pairs Characteristics
Sydney 21:00 06:00 AUD, NZD Low volatility start
Tokyo 00:00 09:00 JPY pairs Moderate activity
London 08:00 17:00 EUR, GBP Highest volume
New York 13:00 22:00 USD pairs High volatility
class ForexSession:
    """
    Represents a forex trading session.
    
    Attributes:
        name: Session name
        open_hour: Opening hour in UTC
        close_hour: Closing hour in UTC
        key_currencies: Most active currencies during session
    """
    
    def __init__(self, name: str, open_hour: int, close_hour: int, 
                 key_currencies: List[str]):
        self.name = name
        self.open_hour = open_hour
        self.close_hour = close_hour
        self.key_currencies = key_currencies
    
    def is_open(self, utc_hour: int) -> bool:
        """Check if the session is open at a given UTC hour."""
        if self.open_hour < self.close_hour:
            return self.open_hour <= utc_hour < self.close_hour
        else:  # Crosses midnight
            return utc_hour >= self.open_hour or utc_hour < self.close_hour
    
    def hours_until_open(self, utc_hour: int) -> int:
        """Calculate hours until session opens."""
        if self.is_open(utc_hour):
            return 0
        hours = (self.open_hour - utc_hour) % 24
        return hours


# Define the four major sessions
SESSIONS = {
    'Sydney': ForexSession('Sydney', 21, 6, ['AUD', 'NZD']),
    'Tokyo': ForexSession('Tokyo', 0, 9, ['JPY', 'AUD']),
    'London': ForexSession('London', 8, 17, ['EUR', 'GBP', 'CHF']),
    'New York': ForexSession('New York', 13, 22, ['USD', 'CAD'])
}

# Check current session status
current_utc_hour = datetime.now(timezone.utc).hour
print(f"Current UTC Hour: {current_utc_hour:02d}:00")
print("\nSession Status")
print("=" * 50)
for name, session in SESSIONS.items():
    status = "OPEN" if session.is_open(current_utc_hour) else "CLOSED"
    hours_until = session.hours_until_open(current_utc_hour)
    hours_info = f"" if hours_until == 0 else f" (opens in {hours_until}h)"
    currencies = ', '.join(session.key_currencies)
    print(f"{name:12} {status:6}{hours_info:15} [{currencies}]")
# Visualize session overlaps
def plot_forex_sessions():
    """Create a visual representation of forex trading sessions."""
    fig, ax = plt.subplots(figsize=(14, 5))
    
    sessions_data = [
        ('Sydney', 21, 6, '#2ecc71'),
        ('Tokyo', 0, 9, '#e74c3c'),
        ('London', 8, 17, '#3498db'),
        ('New York', 13, 22, '#9b59b6')
    ]
    
    for i, (name, start, end, color) in enumerate(sessions_data):
        y = i * 0.8
        
        if start > end:  # Crosses midnight
            # Draw first part (start to midnight)
            ax.barh(y, 24 - start, left=start, height=0.6, color=color, alpha=0.7)
            # Draw second part (midnight to end)
            ax.barh(y, end, left=0, height=0.6, color=color, alpha=0.7)
        else:
            ax.barh(y, end - start, left=start, height=0.6, color=color, alpha=0.7)
        
        ax.text(-0.5, y, name, ha='right', va='center', fontweight='bold', fontsize=11)
    
    # Highlight overlaps
    ax.axvspan(8, 9, alpha=0.2, color='orange', label='Tokyo-London Overlap')
    ax.axvspan(13, 17, alpha=0.2, color='red', label='London-NY Overlap')
    
    ax.set_xlim(-4, 24)
    ax.set_ylim(-0.5, 3.5)
    ax.set_xlabel('UTC Hour', fontsize=12)
    ax.set_xticks(range(0, 25, 2))
    ax.set_xticklabels([f'{h:02d}:00' for h in range(0, 25, 2)])
    ax.set_yticks([])
    ax.set_title('Forex Trading Sessions (UTC)', fontsize=14, fontweight='bold')
    ax.legend(loc='upper right')
    ax.grid(True, axis='x', alpha=0.3)
    
    plt.tight_layout()
    plt.show()

plot_forex_sessions()
# Session volatility analysis
def estimate_session_volatility(pair: str, session: str) -> dict:
    """
    Estimate typical volatility for a pair during a session.
    
    Args:
        pair: Currency pair
        session: Trading session name
        
    Returns:
        Dictionary with volatility estimates
    """
    # Volatility multipliers (base = London session)
    session_multipliers = {
        'Sydney': 0.5,
        'Tokyo': 0.7,
        'London': 1.0,
        'New York': 0.9,
        'London-NY Overlap': 1.2
    }
    
    # Base pip ranges per pair (approximate daily range in London)
    base_ranges = {
        'EUR/USD': 60,
        'GBP/USD': 90,
        'USD/JPY': 70,
        'AUD/USD': 65,
        'USD/CAD': 70
    }
    
    pair = pair.upper().replace('_', '/')
    base_range = base_ranges.get(pair, 75)
    multiplier = session_multipliers.get(session, 1.0)
    
    session_range = base_range * multiplier
    
    return {
        'pair': pair,
        'session': session,
        'expected_range_pips': round(session_range),
        'volatility_level': 'High' if multiplier >= 1.0 else 'Medium' if multiplier >= 0.7 else 'Low'
    }

# Compare volatility across sessions
print("EUR/USD Volatility by Session")
print("=" * 50)
for session in ['Sydney', 'Tokyo', 'London', 'New York', 'London-NY Overlap']:
    vol = estimate_session_volatility('EUR/USD', session)
    print(f"{session:20} {vol['expected_range_pips']:3} pips ({vol['volatility_level']})")

Exercise 1.2: Session Analysis (Guided)

Complete the function to determine active sessions for a given UTC time.

Exercise
Solution 1.2
def get_active_sessions(utc_hour: int) -> dict:
    """
    Determine which forex sessions are active at a given UTC hour.

    Args:
        utc_hour: Hour in UTC (0-23)

    Returns:
        Dictionary with active sessions and trading conditions
    """
    sessions_info = {
        'Sydney': (21, 6),    # 21:00 - 06:00
        'Tokyo': (0, 9),      # 00:00 - 09:00
        'London': (8, 17),    # 08:00 - 17:00
        'New York': (13, 22)  # 13:00 - 22:00
    }

    active = []

    for session, (open_h, close_h) in sessions_info.items():  # Iterate over items
        if open_h > close_h:  # Session crosses midnight
            is_open = utc_hour >= open_h or utc_hour < close_h  # Logical OR
        else:
            is_open = open_h <= utc_hour < close_h

        if is_open:
            active.append(session)  # Add to list

    # Determine trading conditions
    num_active = len(active)
    if num_active >= 2:
        condition = 'High Volatility (Session Overlap)'
    elif num_active == 1:
        condition = 'Normal Volatility'
    else:
        condition = 'Low Volatility (Weekend)'

    return {
        'utc_hour': utc_hour,
        'active_sessions': active,
        'num_sessions': num_active,
        'condition': condition
    }

1.4 Pips & Lots

What is a Pip?

A pip (Percentage in Point) is the smallest price movement in forex:

  • For most pairs: 0.0001 (fourth decimal place)
  • For JPY pairs: 0.01 (second decimal place)
EUR/USD: 1.0850 → 1.0851 = +1 pip
USD/JPY: 150.00 → 150.01 = +1 pip

Lot Sizes

Lot Type Units EUR/USD Pip Value
Standard 100,000 $10 per pip
Mini 10,000 $1 per pip
Micro 1,000 $0.10 per pip
Nano 100 $0.01 per pip
class PipCalculator:
    """
    Calculator for pip values and position sizing.
    
    Attributes:
        pair: Currency pair
        account_currency: Account denomination currency
    """
    
    # Pip sizes for different pair types
    STANDARD_PIP = 0.0001
    JPY_PIP = 0.01
    
    # Lot sizes
    LOT_SIZES = {
        'standard': 100_000,
        'mini': 10_000,
        'micro': 1_000,
        'nano': 100
    }
    
    def __init__(self, pair: str, account_currency: str = 'USD'):
        self.pair = pair.upper().replace('_', '/')
        self.account_currency = account_currency.upper()
        self.base, self.quote = self.pair.split('/')
    
    @property
    def pip_size(self) -> float:
        """Get the pip size for this pair."""
        return self.JPY_PIP if 'JPY' in self.pair else self.STANDARD_PIP
    
    def pip_value(self, lot_size: float = 1.0, exchange_rate: Optional[float] = None) -> float:
        """
        Calculate pip value in account currency.
        
        Args:
            lot_size: Position size in lots
            exchange_rate: Current exchange rate (for conversion if needed)
            
        Returns:
            Pip value in account currency
        """
        units = lot_size * self.LOT_SIZES['standard']
        pip_value_in_quote = units * self.pip_size
        
        # If quote currency matches account currency, we're done
        if self.quote == self.account_currency:
            return pip_value_in_quote
        
        # Otherwise, need to convert (simplified - assumes USD account)
        if exchange_rate is not None:
            if self.account_currency == 'USD' and self.base == 'USD':
                return pip_value_in_quote / exchange_rate
        
        return pip_value_in_quote
    
    def pips_to_price(self, pips: int) -> float:
        """Convert pips to price movement."""
        return pips * self.pip_size
    
    def price_to_pips(self, price_change: float) -> float:
        """Convert price movement to pips."""
        return price_change / self.pip_size


# Example calculations
calc = PipCalculator('EUR/USD')
print("EUR/USD Pip Calculations")
print("=" * 40)
print(f"Pip size: {calc.pip_size}")
print(f"1 standard lot pip value: ${calc.pip_value(1.0):.2f}")
print(f"1 mini lot pip value: ${calc.pip_value(0.1):.2f}")
print(f"1 micro lot pip value: ${calc.pip_value(0.01):.2f}")
print(f"\n50 pips = {calc.pips_to_price(50):.4f} price movement")

print("\n" + "=" * 40)
calc_jpy = PipCalculator('USD/JPY')
print(f"USD/JPY pip size: {calc_jpy.pip_size}")
print(f"50 pips = {calc_jpy.pips_to_price(50):.2f} price movement")
# Pip value comparison across pairs
def compare_pip_values(pairs: List[str], lot_size: float = 1.0) -> pd.DataFrame:
    """
    Compare pip values across different currency pairs.
    
    Args:
        pairs: List of currency pairs
        lot_size: Position size in lots
        
    Returns:
        DataFrame with pip value comparison
    """
    results = []
    
    for pair in pairs:
        calc = PipCalculator(pair)
        pip_val = calc.pip_value(lot_size)
        
        results.append({
            'Pair': pair,
            'Pip Size': calc.pip_size,
            f'Pip Value ({lot_size} lot)': f'${pip_val:.2f}',
            '10 Pip Move': f'${pip_val * 10:.2f}',
            '50 Pip Move': f'${pip_val * 50:.2f}'
        })
    
    return pd.DataFrame(results)

# Compare major pairs
pairs = ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CAD']
print("Pip Value Comparison (1 Standard Lot)")
print("=" * 70)
print(compare_pip_values(pairs).to_string(index=False))

Exercise 1.3: Calculate Pip Values (Guided)

Complete the function to calculate profit/loss in pips and monetary value.

Exercise
Solution 1.3
def calculate_trade_pnl(
    pair: str,
    entry_price: float,
    exit_price: float,
    lot_size: float,
    direction: str  # 'long' or 'short'
) -> dict:
    """
    Calculate profit/loss for a forex trade.

    Args:
        pair: Currency pair
        entry_price: Entry price
        exit_price: Exit price
        lot_size: Position size in lots
        direction: Trade direction ('long' or 'short')

    Returns:
        Dictionary with P&L details
    """
    calc = PipCalculator(pair)

    # Calculate price change
    price_change = exit_price - entry_price

    # Adjust for direction
    if direction.lower() == 'short':  # Short position
        price_change = -price_change

    # Convert to pips
    pips_gained = calc.price_to_pips(price_change)  # Convert price to pips

    # Calculate monetary P&L
    pip_value = calc.pip_value(lot_size)
    monetary_pnl = pips_gained * pip_value  # Multiply pips by pip value

    return {
        'pair': pair,
        'direction': direction,
        'entry': entry_price,
        'exit': exit_price,
        'lots': lot_size,
        'pips': round(pips_gained, 1),
        'pnl': round(monetary_pnl, 2)
    }

Exercise 1.4: Build Currency Pair Analyzer (Open-ended)

Build a comprehensive currency pair analyzer class that includes: - Pair classification (major/minor/exotic) - Pip calculations - Spread cost analysis - Position sizing based on risk

Your implementation:

Exercise
Solution 1.4
class CurrencyPairAnalyzer:
    """
    Comprehensive analyzer for forex currency pairs.

    Attributes:
        pair: Currency pair symbol
        base: Base currency
        quote: Quote currency
    """

    MAJOR_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD']
    EXOTIC_CURRENCIES = ['TRY', 'ZAR', 'MXN', 'SGD', 'HKD', 'SEK', 'NOK', 'PLN']

    TYPICAL_SPREADS = {
        'Major': 1.0,
        'Minor': 3.0,
        'Exotic': 15.0
    }

    def __init__(self, pair: str):
        self.pair = pair.upper().replace('_', '/')
        self.base, self.quote = self.pair.split('/')
        self._pip_size = 0.01 if 'JPY' in self.pair else 0.0001

    def classify(self) -> str:
        """Classify the pair as major, minor, or exotic."""
        has_usd = 'USD' in self.pair
        base_major = self.base in self.MAJOR_CURRENCIES
        quote_major = self.quote in self.MAJOR_CURRENCIES

        if has_usd and base_major and quote_major:
            return 'Major'
        elif base_major and quote_major:
            return 'Minor'
        else:
            return 'Exotic'

    def pip_value(self, lots: float = 1.0) -> float:
        """Calculate pip value in USD."""
        units = lots * 100_000
        return units * self._pip_size

    def spread_cost(self, lots: float, spread_pips: float = None) -> float:
        """Calculate spread cost for a position."""
        if spread_pips is None:
            spread_pips = self.TYPICAL_SPREADS[self.classify()]
        return self.pip_value(lots) * spread_pips

    def position_size(
        self,
        account_balance: float,
        risk_pct: float,
        stop_loss_pips: int
    ) -> float:
        """Calculate position size based on risk parameters."""
        risk_amount = account_balance * (risk_pct / 100)
        pip_value_per_lot = self.pip_value(1.0)
        risk_per_lot = pip_value_per_lot * stop_loss_pips
        return round(risk_amount / risk_per_lot, 2)

    def summary(self) -> dict:
        """Get a comprehensive summary of the pair."""
        return {
            'pair': self.pair,
            'base': self.base,
            'quote': self.quote,
            'classification': self.classify(),
            'pip_size': self._pip_size,
            'pip_value_std_lot': f'${self.pip_value(1.0):.2f}',
            'typical_spread': f'{self.TYPICAL_SPREADS[self.classify()]} pips',
            'spread_cost_std_lot': f'${self.spread_cost(1.0):.2f}'
        }


# Test the analyzer
for pair in ['EUR/USD', 'GBP/JPY', 'USD/TRY']:
    analyzer = CurrencyPairAnalyzer(pair)
    summary = analyzer.summary()
    print(f"\n{pair} Analysis:")
    for key, value in summary.items():
        print(f"  {key}: {value}")

    # Position sizing example
    lots = analyzer.position_size(10000, 2, 50)
    print(f"  Position size ($10k, 2% risk, 50 pip SL): {lots} lots")

Exercise 1.5: Session-Based Trading Filter (Open-ended)

Build a session-based trading filter that determines optimal trading times for different currency pairs based on active sessions.

Your implementation:

Exercise
Solution 1.5
class SessionTradingFilter:
    """
    Filter trading times based on forex sessions.

    Helps identify optimal trading windows for different pairs.
    """

    SESSIONS = {
        'Sydney': {'open': 21, 'close': 6, 'currencies': ['AUD', 'NZD']},
        'Tokyo': {'open': 0, 'close': 9, 'currencies': ['JPY', 'AUD']},
        'London': {'open': 8, 'close': 17, 'currencies': ['EUR', 'GBP', 'CHF']},
        'New York': {'open': 13, 'close': 22, 'currencies': ['USD', 'CAD']}
    }

    def __init__(self):
        pass

    def is_session_open(self, session_name: str, utc_hour: int) -> bool:
        """Check if a session is open at a given UTC hour."""
        session = self.SESSIONS.get(session_name)
        if not session:
            return False

        open_h, close_h = session['open'], session['close']

        if open_h > close_h:  # Crosses midnight
            return utc_hour >= open_h or utc_hour < close_h
        return open_h <= utc_hour < close_h

    def get_active_sessions(self, utc_hour: int) -> List[str]:
        """Get all active sessions at a given UTC hour."""
        return [
            name for name in self.SESSIONS
            if self.is_session_open(name, utc_hour)
        ]

    def _get_pair_sessions(self, pair: str) -> List[str]:
        """Get relevant sessions for a currency pair."""
        pair = pair.upper().replace('_', '/')
        base, quote = pair.split('/')

        relevant = []
        for session_name, info in self.SESSIONS.items():
            if base in info['currencies'] or quote in info['currencies']:
                relevant.append(session_name)
        return relevant

    def is_good_time_to_trade(self, pair: str, utc_hour: int) -> dict:
        """Determine if it's a good time to trade a pair."""
        active = self.get_active_sessions(utc_hour)
        relevant = self._get_pair_sessions(pair)

        # Check if relevant sessions are active
        matching = [s for s in relevant if s in active]

        if len(matching) >= 2:
            rating = 'Excellent'
            reason = 'Multiple relevant sessions overlap'
        elif len(matching) == 1:
            rating = 'Good'
            reason = f'{matching[0]} session active'
        elif len(active) > 0:
            rating = 'Fair'
            reason = 'Active sessions but not optimal for this pair'
        else:
            rating = 'Poor'
            reason = 'No major sessions active'

        return {
            'pair': pair,
            'utc_hour': utc_hour,
            'rating': rating,
            'reason': reason,
            'active_sessions': active,
            'relevant_sessions': relevant
        }

    def get_optimal_hours(self, pair: str) -> List[int]:
        """Get optimal trading hours for a pair."""
        optimal = []
        for hour in range(24):
            result = self.is_good_time_to_trade(pair, hour)
            if result['rating'] in ['Excellent', 'Good']:
                optimal.append(hour)
        return optimal


# Test the filter
filter = SessionTradingFilter()

# Check current conditions
current_hour = datetime.now(timezone.utc).hour
for pair in ['EUR/USD', 'AUD/JPY', 'GBP/USD']:
    result = filter.is_good_time_to_trade(pair, current_hour)
    print(f"\n{pair} at {current_hour}:00 UTC:")
    print(f"  Rating: {result['rating']}")
    print(f"  Reason: {result['reason']}")
    print(f"  Optimal hours: {filter.get_optimal_hours(pair)}")

Exercise 1.6: Multi-Pair Market Scanner (Open-ended)

Build a market scanner that analyzes multiple currency pairs and ranks them by trading opportunity.

Your implementation:

Exercise
Solution 1.6
class ForexMarketScanner:
    """
    Scanner for analyzing multiple forex pairs.

    Ranks pairs by opportunity based on volatility and session.
    """

    def __init__(self):
        self.pairs = {}
        self.session_filter = SessionTradingFilter()

    def add_pair(self, pair: str, current_price: float, daily_range: float):
        """Add a pair with its current market data."""
        pair = pair.upper().replace('_', '/')
        self.pairs[pair] = {
            'price': current_price,
            'daily_range': daily_range,
            'analyzer': CurrencyPairAnalyzer(pair)
        }

    def analyze_pair(self, pair: str, utc_hour: int) -> dict:
        """Analyze a single pair for trading opportunity."""
        pair = pair.upper().replace('_', '/')
        if pair not in self.pairs:
            return {'error': 'Pair not found'}

        data = self.pairs[pair]
        analyzer = data['analyzer']

        # Get session rating
        session_info = self.session_filter.is_good_time_to_trade(pair, utc_hour)

        # Calculate volatility score (daily range in pips)
        pip_size = 0.01 if 'JPY' in pair else 0.0001
        range_pips = data['daily_range'] / pip_size

        # Score calculation
        session_scores = {'Excellent': 4, 'Good': 3, 'Fair': 2, 'Poor': 1}
        session_score = session_scores.get(session_info['rating'], 1)

        # Normalize volatility (assume 100 pips is high)
        volatility_score = min(range_pips / 25, 4)  # Max 4 points

        # Classification bonus
        class_bonus = {'Major': 1.0, 'Minor': 0.8, 'Exotic': 0.6}

        total_score = (session_score + volatility_score) * class_bonus.get(
            analyzer.classify(), 1.0
        )

        return {
            'pair': pair,
            'classification': analyzer.classify(),
            'price': data['price'],
            'daily_range_pips': round(range_pips, 1),
            'session_rating': session_info['rating'],
            'opportunity_score': round(total_score, 2),
            'spread_cost': analyzer.spread_cost(1.0)
        }

    def scan_all(self, utc_hour: int) -> pd.DataFrame:
        """Scan all pairs and return ranked results."""
        results = []
        for pair in self.pairs:
            analysis = self.analyze_pair(pair, utc_hour)
            if 'error' not in analysis:
                results.append(analysis)

        df = pd.DataFrame(results)
        if not df.empty:
            df = df.sort_values('opportunity_score', ascending=False)
        return df

    def get_top_opportunities(self, utc_hour: int, n: int = 5) -> List[str]:
        """Get top N pairs by opportunity."""
        df = self.scan_all(utc_hour)
        if df.empty:
            return []
        return df.head(n)['pair'].tolist()


# Test the scanner
scanner = ForexMarketScanner()

# Add sample pairs with mock data
pairs_data = [
    ('EUR/USD', 1.0850, 0.0065),
    ('GBP/USD', 1.2650, 0.0095),
    ('USD/JPY', 150.50, 0.85),
    ('AUD/USD', 0.6550, 0.0055),
    ('EUR/GBP', 0.8580, 0.0045),
    ('USD/CAD', 1.3650, 0.0070),
    ('USD/TRY', 32.50, 1.25)
]

for pair, price, range_val in pairs_data:
    scanner.add_pair(pair, price, range_val)

# Scan at current time
current_hour = datetime.now(timezone.utc).hour
print(f"\nMarket Scan at {current_hour}:00 UTC")
print("=" * 80)
results = scanner.scan_all(current_hour)
print(results.to_string(index=False))

print(f"\nTop Opportunities: {scanner.get_top_opportunities(current_hour, 3)}")

Module Project: Forex Market Analyzer

Build a comprehensive forex market analyzer that combines all concepts from this module.

class ForexMarketAnalyzer:
    """
    Comprehensive forex market analyzer.
    
    Combines pair analysis, session tracking, and pip calculations
    into a unified interface for forex market analysis.
    
    Attributes:
        account_currency: Base currency for calculations
        account_balance: Trading account balance
    """
    
    MAJOR_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD']
    
    SESSIONS = {
        'Sydney': {'open': 21, 'close': 6, 'currencies': ['AUD', 'NZD']},
        'Tokyo': {'open': 0, 'close': 9, 'currencies': ['JPY', 'AUD', 'NZD']},
        'London': {'open': 8, 'close': 17, 'currencies': ['EUR', 'GBP', 'CHF']},
        'New York': {'open': 13, 'close': 22, 'currencies': ['USD', 'CAD']}
    }
    
    TYPICAL_SPREADS = {'Major': 1.0, 'Minor': 3.0, 'Exotic': 15.0}
    
    def __init__(self, account_currency: str = 'USD', account_balance: float = 10000):
        self.account_currency = account_currency.upper()
        self.account_balance = account_balance
        self.watched_pairs = {}
    
    # ==================== Pair Analysis ====================
    
    def add_pair(self, pair: str, bid: float = None, ask: float = None):
        """
        Add a currency pair to the watchlist.
        
        Args:
            pair: Currency pair symbol
            bid: Current bid price
            ask: Current ask price
        """
        pair = self._normalize_pair(pair)
        base, quote = pair.split('/')
        
        self.watched_pairs[pair] = {
            'base': base,
            'quote': quote,
            'bid': bid,
            'ask': ask,
            'classification': self._classify_pair(pair)
        }
    
    def _normalize_pair(self, pair: str) -> str:
        """Normalize pair format to BASE/QUOTE."""
        return pair.upper().replace('_', '/')
    
    def _classify_pair(self, pair: str) -> str:
        """Classify pair as Major, Minor, or Exotic."""
        base, quote = pair.split('/')
        has_usd = 'USD' in pair
        both_major = base in self.MAJOR_CURRENCIES and quote in self.MAJOR_CURRENCIES
        
        if has_usd and both_major:
            return 'Major'
        elif both_major:
            return 'Minor'
        return 'Exotic'
    
    def get_pair_info(self, pair: str) -> dict:
        """Get comprehensive information about a pair."""
        pair = self._normalize_pair(pair)
        
        if pair not in self.watched_pairs:
            self.add_pair(pair)
        
        info = self.watched_pairs[pair].copy()
        info['pip_size'] = self._get_pip_size(pair)
        info['pip_value_std_lot'] = self._calculate_pip_value(pair, 1.0)
        info['typical_spread'] = self.TYPICAL_SPREADS[info['classification']]
        
        if info['bid'] and info['ask']:
            info['current_spread'] = (info['ask'] - info['bid']) / info['pip_size']
        
        return info
    
    # ==================== Pip Calculations ====================
    
    def _get_pip_size(self, pair: str) -> float:
        """Get pip size for a pair."""
        return 0.01 if 'JPY' in pair else 0.0001
    
    def _calculate_pip_value(self, pair: str, lots: float) -> float:
        """Calculate pip value in account currency."""
        pip_size = self._get_pip_size(pair)
        units = lots * 100_000
        return units * pip_size
    
    def calculate_pips(self, pair: str, entry: float, exit: float) -> float:
        """Calculate pip difference between two prices."""
        pair = self._normalize_pair(pair)
        pip_size = self._get_pip_size(pair)
        return (exit - entry) / pip_size
    
    def calculate_pnl(
        self,
        pair: str,
        entry: float,
        exit: float,
        lots: float,
        direction: str = 'long'
    ) -> dict:
        """Calculate P&L for a trade."""
        pair = self._normalize_pair(pair)
        
        # Calculate pip change
        pips = self.calculate_pips(pair, entry, exit)
        if direction.lower() == 'short':
            pips = -pips
        
        # Calculate monetary P&L
        pip_value = self._calculate_pip_value(pair, lots)
        pnl = pips * pip_value / 100_000 * lots * 100_000
        
        # Simplified: pip_value for 1 lot * pips
        pip_value_per_lot = self._calculate_pip_value(pair, 1.0)
        pnl = pips * (pip_value_per_lot / (1 / self._get_pip_size(pair))) * lots
        
        # Correct calculation
        pnl = pips * lots * 10  # $10 per pip per lot for most pairs
        
        return {
            'pair': pair,
            'direction': direction,
            'entry': entry,
            'exit': exit,
            'lots': lots,
            'pips': round(pips, 1),
            'pnl': round(pnl, 2)
        }
    
    # ==================== Position Sizing ====================
    
    def calculate_position_size(
        self,
        pair: str,
        stop_loss_pips: int,
        risk_percent: float = 1.0
    ) -> dict:
        """
        Calculate position size based on risk parameters.
        
        Args:
            pair: Currency pair
            stop_loss_pips: Stop loss distance in pips
            risk_percent: Percentage of account to risk
            
        Returns:
            Position sizing details
        """
        pair = self._normalize_pair(pair)
        
        # Calculate risk amount
        risk_amount = self.account_balance * (risk_percent / 100)
        
        # Pip value per standard lot (approximately $10 for most pairs)
        pip_value_per_lot = 10  # Simplified
        
        # Risk per lot = pip_value * stop_loss_pips
        risk_per_lot = pip_value_per_lot * stop_loss_pips
        
        # Position size
        lots = risk_amount / risk_per_lot
        
        return {
            'pair': pair,
            'account_balance': self.account_balance,
            'risk_percent': risk_percent,
            'risk_amount': round(risk_amount, 2),
            'stop_loss_pips': stop_loss_pips,
            'position_lots': round(lots, 2),
            'position_units': int(lots * 100_000)
        }
    
    # ==================== Session Analysis ====================
    
    def _is_session_open(self, session_name: str, utc_hour: int) -> bool:
        """Check if a session is open."""
        session = self.SESSIONS.get(session_name)
        if not session:
            return False
        
        open_h, close_h = session['open'], session['close']
        if open_h > close_h:
            return utc_hour >= open_h or utc_hour < close_h
        return open_h <= utc_hour < close_h
    
    def get_active_sessions(self, utc_hour: int = None) -> List[str]:
        """Get currently active sessions."""
        if utc_hour is None:
            utc_hour = datetime.now(timezone.utc).hour
        
        return [
            name for name in self.SESSIONS
            if self._is_session_open(name, utc_hour)
        ]
    
    def analyze_trading_conditions(self, pair: str, utc_hour: int = None) -> dict:
        """
        Analyze current trading conditions for a pair.
        
        Args:
            pair: Currency pair
            utc_hour: Hour in UTC (default: current)
            
        Returns:
            Trading condition analysis
        """
        if utc_hour is None:
            utc_hour = datetime.now(timezone.utc).hour
        
        pair = self._normalize_pair(pair)
        base, quote = pair.split('/')
        
        # Get active sessions
        active = self.get_active_sessions(utc_hour)
        
        # Find relevant sessions for this pair
        relevant = []
        for session_name, info in self.SESSIONS.items():
            if base in info['currencies'] or quote in info['currencies']:
                relevant.append(session_name)
        
        # Determine trading condition
        matching = [s for s in relevant if s in active]
        
        if len(matching) >= 2:
            rating = 'Excellent'
            expected_volatility = 'High'
        elif len(matching) == 1:
            rating = 'Good'
            expected_volatility = 'Medium-High'
        elif len(active) > 0:
            rating = 'Fair'
            expected_volatility = 'Medium'
        else:
            rating = 'Poor'
            expected_volatility = 'Low'
        
        return {
            'pair': pair,
            'utc_hour': utc_hour,
            'active_sessions': active,
            'relevant_sessions': relevant,
            'matching_sessions': matching,
            'trading_rating': rating,
            'expected_volatility': expected_volatility
        }
    
    # ==================== Market Overview ====================
    
    def get_market_overview(self, utc_hour: int = None) -> pd.DataFrame:
        """Get overview of all watched pairs."""
        if utc_hour is None:
            utc_hour = datetime.now(timezone.utc).hour
        
        results = []
        for pair in self.watched_pairs:
            info = self.get_pair_info(pair)
            conditions = self.analyze_trading_conditions(pair, utc_hour)
            
            results.append({
                'Pair': pair,
                'Type': info['classification'],
                'Pip Value': f"${info['pip_value_std_lot']:.2f}",
                'Typ. Spread': f"{info['typical_spread']} pips",
                'Rating': conditions['trading_rating'],
                'Volatility': conditions['expected_volatility']
            })
        
        return pd.DataFrame(results)
    
    def print_summary(self):
        """Print a comprehensive market summary."""
        utc_hour = datetime.now(timezone.utc).hour
        active_sessions = self.get_active_sessions(utc_hour)
        
        print("\n" + "=" * 70)
        print("FOREX MARKET ANALYZER SUMMARY")
        print("=" * 70)
        print(f"\nAccount Balance: ${self.account_balance:,.2f}")
        print(f"Current UTC Time: {utc_hour:02d}:00")
        print(f"Active Sessions: {', '.join(active_sessions) if active_sessions else 'None'}")
        
        if self.watched_pairs:
            print("\n" + "-" * 70)
            print("WATCHED PAIRS")
            print("-" * 70)
            overview = self.get_market_overview(utc_hour)
            print(overview.to_string(index=False))
        
        print("\n" + "=" * 70)
# Demonstrate the Forex Market Analyzer

# Create analyzer with $25,000 account
analyzer = ForexMarketAnalyzer(account_balance=25000)

# Add pairs to watchlist
pairs_to_watch = [
    ('EUR/USD', 1.0848, 1.0850),
    ('GBP/USD', 1.2648, 1.2652),
    ('USD/JPY', 150.48, 150.51),
    ('AUD/USD', 0.6548, 0.6551),
    ('EUR/GBP', 0.8578, 0.8582),
    ('USD/CAD', 1.3648, 1.3652)
]

for pair, bid, ask in pairs_to_watch:
    analyzer.add_pair(pair, bid, ask)

# Print summary
analyzer.print_summary()
# Example: Position sizing calculation
print("Position Sizing Examples")
print("=" * 50)

# Calculate position size for 1% risk with 50 pip stop
for pair in ['EUR/USD', 'GBP/USD', 'USD/JPY']:
    sizing = analyzer.calculate_position_size(pair, stop_loss_pips=50, risk_percent=1.0)
    print(f"\n{pair}:")
    print(f"  Risk: ${sizing['risk_amount']:.2f} (1% of ${sizing['account_balance']:,.0f})")
    print(f"  Stop Loss: {sizing['stop_loss_pips']} pips")
    print(f"  Position: {sizing['position_lots']:.2f} lots ({sizing['position_units']:,} units)")
# Example: Trade P&L calculation
print("\nTrade P&L Examples")
print("=" * 60)

# Simulate some trades
trades = [
    ('EUR/USD', 1.0850, 1.0900, 0.5, 'long'),   # 50 pip win
    ('GBP/USD', 1.2700, 1.2650, 0.3, 'short'),  # 50 pip win short
    ('USD/JPY', 150.00, 149.60, 0.2, 'short'),  # 40 pip win
]

for pair, entry, exit_price, lots, direction in trades:
    result = analyzer.calculate_pnl(pair, entry, exit_price, lots, direction)
    sign = '+' if result['pnl'] > 0 else ''
    print(f"{result['pair']} {result['direction'].upper():5} {result['lots']:.1f} lots: "
          f"{result['entry']}{result['exit']} = {sign}{result['pips']} pips (${sign}{result['pnl']:.2f})")
# Example: Session analysis
print("\nSession Analysis for Different Hours")
print("=" * 70)

test_hours = [3, 8, 14, 20]
pair = 'EUR/USD'

for hour in test_hours:
    conditions = analyzer.analyze_trading_conditions(pair, hour)
    print(f"\n{pair} at {hour:02d}:00 UTC:")
    print(f"  Active Sessions: {', '.join(conditions['active_sessions']) if conditions['active_sessions'] else 'None'}")
    print(f"  Relevant Sessions: {', '.join(conditions['relevant_sessions'])}")
    print(f"  Trading Rating: {conditions['trading_rating']}")
    print(f"  Expected Volatility: {conditions['expected_volatility']}")

Key Takeaways

  • Currency pairs consist of a base currency and quote currency - buying EUR/USD means buying euros and selling dollars
  • Major pairs include USD and offer the tightest spreads; minor pairs are crosses between major currencies; exotic pairs include emerging market currencies
  • The forex market is decentralized (OTC) with trading flowing through a hierarchy from interbank to retail
  • Four major trading sessions (Sydney, Tokyo, London, New York) provide 24/5 market access with session overlaps offering the highest volatility
  • Pips are the standard price movement unit (0.0001 for most pairs, 0.01 for JPY pairs)
  • Lot sizes standardize position sizes: standard (100k), mini (10k), micro (1k)
  • Position sizing based on risk percentage and stop loss is critical for account management

Next: Module 2 - Futures Market Basics

Module 2: Futures Market Basics

Part 1: Market Fundamentals

Duration Exercises
~2.5 hours 6

Learning Objectives

By the end of this module, you will be able to:

  • Understand futures contract specifications and mechanics
  • Navigate popular futures markets (indices, commodities, currencies)
  • Explain the relationship between spot and futures prices
  • Understand contango, backwardation, and basis
  • Handle contract expiration and rollover
  • Build continuous futures price series
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum

# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')

2.1 What are Futures?

A futures contract is a standardized agreement to buy or sell an underlying asset at a predetermined price on a specific future date.

Key Characteristics

Feature Description
Standardized Exchange-defined contract size, quality, delivery
Leverage Only margin required, not full contract value
Expiration Contracts expire on specific dates
Settlement Physical delivery or cash settlement
Mark-to-Market Daily profit/loss settlement

Contract Specifications

Every futures contract has specific details defined by the exchange:

E-mini S&P 500 (ES) Contract Specifications:
├── Exchange: CME
├── Contract Size: $50 × S&P 500 Index
├── Tick Size: 0.25 index points
├── Tick Value: $12.50
├── Trading Hours: Nearly 24 hours (Sunday-Friday)
├── Expiration: March, June, September, December
└── Settlement: Cash settled
class SettlementType(Enum):
    """Types of futures settlement."""
    CASH = "cash"
    PHYSICAL = "physical"


@dataclass
class FuturesContract:
    """
    Represents a futures contract specification.
    
    Attributes:
        symbol: Contract symbol (e.g., 'ES')
        name: Full contract name
        exchange: Trading exchange
        contract_size: Size multiplier
        tick_size: Minimum price movement
        tick_value: Dollar value per tick
        settlement: Settlement type
        months: Trading months (letter codes)
    """
    symbol: str
    name: str
    exchange: str
    contract_size: float
    tick_size: float
    tick_value: float
    settlement: SettlementType
    months: List[str]
    
    @property
    def point_value(self) -> float:
        """Calculate the dollar value of one full point move."""
        ticks_per_point = 1 / self.tick_size
        return ticks_per_point * self.tick_value
    
    def calculate_pnl(self, entry: float, exit: float, contracts: int = 1) -> float:
        """Calculate P&L for a trade."""
        points = exit - entry
        return points * self.point_value * contracts
    
    def ticks_to_points(self, ticks: int) -> float:
        """Convert ticks to points."""
        return ticks * self.tick_size
    
    def points_to_ticks(self, points: float) -> int:
        """Convert points to ticks."""
        return int(points / self.tick_size)


# Define popular futures contracts
FUTURES_CONTRACTS = {
    'ES': FuturesContract(
        symbol='ES',
        name='E-mini S&P 500',
        exchange='CME',
        contract_size=50,
        tick_size=0.25,
        tick_value=12.50,
        settlement=SettlementType.CASH,
        months=['H', 'M', 'U', 'Z']  # Mar, Jun, Sep, Dec
    ),
    'NQ': FuturesContract(
        symbol='NQ',
        name='E-mini Nasdaq 100',
        exchange='CME',
        contract_size=20,
        tick_size=0.25,
        tick_value=5.00,
        settlement=SettlementType.CASH,
        months=['H', 'M', 'U', 'Z']
    ),
    'CL': FuturesContract(
        symbol='CL',
        name='Crude Oil',
        exchange='NYMEX',
        contract_size=1000,  # barrels
        tick_size=0.01,
        tick_value=10.00,
        settlement=SettlementType.PHYSICAL,
        months=['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z']
    ),
    'GC': FuturesContract(
        symbol='GC',
        name='Gold',
        exchange='COMEX',
        contract_size=100,  # troy ounces
        tick_size=0.10,
        tick_value=10.00,
        settlement=SettlementType.PHYSICAL,
        months=['G', 'J', 'M', 'Q', 'V', 'Z']
    ),
    '6E': FuturesContract(
        symbol='6E',
        name='Euro FX',
        exchange='CME',
        contract_size=125000,  # euros
        tick_size=0.00005,
        tick_value=6.25,
        settlement=SettlementType.CASH,
        months=['H', 'M', 'U', 'Z']
    ),
    'ZB': FuturesContract(
        symbol='ZB',
        name='30-Year Treasury Bond',
        exchange='CBOT',
        contract_size=100000,
        tick_size=1/32,
        tick_value=31.25,
        settlement=SettlementType.PHYSICAL,
        months=['H', 'M', 'U', 'Z']
    )
}

# Display contract specifications
print("Popular Futures Contract Specifications")
print("=" * 80)
for symbol, contract in FUTURES_CONTRACTS.items():
    print(f"\n{contract.name} ({symbol})")
    print(f"  Exchange: {contract.exchange}")
    print(f"  Contract Size: {contract.contract_size:,}")
    print(f"  Tick Size: {contract.tick_size}")
    print(f"  Tick Value: ${contract.tick_value:.2f}")
    print(f"  Point Value: ${contract.point_value:.2f}")
    print(f"  Settlement: {contract.settlement.value}")
# Demonstrate P&L calculations
es = FUTURES_CONTRACTS['ES']

print("E-mini S&P 500 (ES) P&L Examples")
print("=" * 50)

# Example trades
trades = [
    (5000.00, 5010.00, 1, 'Long'),   # 10 point gain
    (5000.00, 4990.00, 2, 'Long'),   # 10 point loss x2
    (5000.00, 4980.00, 1, 'Short'),  # 20 point gain (short)
]

for entry, exit_price, contracts, direction in trades:
    if direction == 'Short':
        pnl = es.calculate_pnl(exit_price, entry, contracts)  # Reversed for short
    else:
        pnl = es.calculate_pnl(entry, exit_price, contracts)
    
    points = abs(exit_price - entry)
    ticks = es.points_to_ticks(points)
    sign = '+' if pnl > 0 else ''
    
    print(f"{direction:5} {contracts} contract(s): {entry}{exit_price} = "
          f"{points} pts ({ticks} ticks) = {sign}${pnl:,.2f}")

2.2 Popular Futures Markets

Futures are traded on various underlying assets:

Market Categories

Category Examples Key Contracts
Equity Indices S&P 500, Nasdaq, Dow ES, NQ, YM
Energy Crude Oil, Natural Gas CL, NG
Metals Gold, Silver, Copper GC, SI, HG
Currencies Euro, Yen, Pound 6E, 6J, 6B
Interest Rates Treasury Bonds, Eurodollar ZB, ZN, GE
Agriculture Corn, Wheat, Soybeans ZC, ZW, ZS
# Futures market categories
FUTURES_CATEGORIES = {
    'Equity Indices': {
        'description': 'Stock market index futures',
        'contracts': {
            'ES': 'E-mini S&P 500',
            'NQ': 'E-mini Nasdaq 100',
            'YM': 'E-mini Dow Jones',
            'RTY': 'E-mini Russell 2000',
            'MES': 'Micro E-mini S&P 500'
        },
        'key_drivers': ['Economic data', 'Earnings', 'Fed policy', 'Geopolitics']
    },
    'Energy': {
        'description': 'Oil, gas, and energy products',
        'contracts': {
            'CL': 'Crude Oil (WTI)',
            'BZ': 'Brent Crude Oil',
            'NG': 'Natural Gas',
            'RB': 'RBOB Gasoline',
            'HO': 'Heating Oil'
        },
        'key_drivers': ['OPEC decisions', 'Inventory data', 'Weather', 'Geopolitics']
    },
    'Metals': {
        'description': 'Precious and industrial metals',
        'contracts': {
            'GC': 'Gold',
            'SI': 'Silver',
            'HG': 'Copper',
            'PL': 'Platinum',
            'PA': 'Palladium'
        },
        'key_drivers': ['USD strength', 'Inflation', 'Safe haven flows', 'Industrial demand']
    },
    'Currencies': {
        'description': 'Currency futures (CME)',
        'contracts': {
            '6E': 'Euro FX',
            '6J': 'Japanese Yen',
            '6B': 'British Pound',
            '6A': 'Australian Dollar',
            '6C': 'Canadian Dollar'
        },
        'key_drivers': ['Interest rates', 'Central bank policy', 'Economic data']
    },
    'Interest Rates': {
        'description': 'Treasury and interest rate futures',
        'contracts': {
            'ZB': '30-Year Treasury Bond',
            'ZN': '10-Year Treasury Note',
            'ZF': '5-Year Treasury Note',
            'ZT': '2-Year Treasury Note',
            'SR3': '3-Month SOFR'
        },
        'key_drivers': ['Fed policy', 'Inflation expectations', 'Economic growth']
    },
    'Agriculture': {
        'description': 'Grains, softs, and livestock',
        'contracts': {
            'ZC': 'Corn',
            'ZW': 'Wheat',
            'ZS': 'Soybeans',
            'KC': 'Coffee',
            'LE': 'Live Cattle'
        },
        'key_drivers': ['Weather', 'USDA reports', 'Global demand', 'Planting/harvest']
    }
}

# Display market categories
print("Futures Market Categories")
print("=" * 70)
for category, info in FUTURES_CATEGORIES.items():
    print(f"\n{category}")
    print(f"  {info['description']}")
    print(f"  Contracts: {', '.join(info['contracts'].keys())}")
    print(f"  Key Drivers: {', '.join(info['key_drivers'][:3])}")
# Compare contract sizes and margins
def get_notional_value(contract: FuturesContract, price: float) -> float:
    """Calculate the notional value of a futures contract."""
    if contract.symbol in ['ES', 'NQ', 'YM']:  # Index futures
        return contract.contract_size * price
    elif contract.symbol == 'CL':  # Crude oil
        return contract.contract_size * price  # 1000 barrels * price
    elif contract.symbol == 'GC':  # Gold
        return contract.contract_size * price  # 100 oz * price
    elif contract.symbol == '6E':  # Euro FX
        return contract.contract_size * price  # 125000 euros * rate
    else:
        return contract.contract_size * price


# Sample prices for calculations
sample_prices = {
    'ES': 5000,
    'NQ': 17500,
    'CL': 75.00,
    'GC': 2050,
    '6E': 1.0850
}

# Approximate initial margins (varies by broker)
initial_margins = {
    'ES': 12000,
    'NQ': 16000,
    'CL': 6000,
    'GC': 8000,
    '6E': 2500
}

print("Futures Contract Size Comparison")
print("=" * 70)
print(f"{'Contract':<12} {'Price':<12} {'Notional':<15} {'Margin':<12} {'Leverage'}")
print("-" * 70)

for symbol in ['ES', 'NQ', 'CL', 'GC', '6E']:
    contract = FUTURES_CONTRACTS[symbol]
    price = sample_prices[symbol]
    notional = get_notional_value(contract, price)
    margin = initial_margins[symbol]
    leverage = notional / margin
    
    print(f"{symbol:<12} ${price:<11,.2f} ${notional:<14,.0f} ${margin:<11,} {leverage:.1f}:1")

Exercise 2.1: Read Contract Specifications (Guided)

Complete the function to parse and analyze futures contract specifications.

Exercise
Solution 2.1
def analyze_contract(symbol: str, current_price: float) -> dict:
    """
    Analyze a futures contract's specifications and calculate key metrics.

    Args:
        symbol: Contract symbol (e.g., 'ES')
        current_price: Current contract price

    Returns:
        Dictionary with contract analysis
    """
    # Get contract from our definitions
    contract = FUTURES_CONTRACTS.get(symbol.upper())  # Get contract by key

    if contract is None:
        return {'error': f'Contract {symbol} not found'}

    # Calculate notional value
    notional = contract.contract_size * current_price  # Multiply by price

    # Calculate value of 1% move
    one_percent_move = current_price * 0.01  # 1% = 0.01
    one_percent_pnl = one_percent_move * contract.point_value

    # Calculate ticks in a 1% move
    ticks_in_one_percent = contract.points_to_ticks(one_percent_move)

    return {
        'symbol': symbol.upper(),
        'name': contract.name,
        'exchange': contract.exchange,
        'current_price': current_price,
        'notional_value': round(notional, 2),
        'point_value': contract.point_value,
        'tick_size': contract.tick_size,
        'tick_value': contract.tick_value,
        'one_percent_move_points': round(one_percent_move, 2),
        'one_percent_move_ticks': ticks_in_one_percent,
        'one_percent_pnl': round(one_percent_pnl, 2)
    }

2.3 Futures vs Spot

Understanding Basis

The basis is the difference between the futures price and the spot price:

Basis = Futures Price - Spot Price

Contango vs Backwardation

Condition Basis Futures vs Spot Common In
Contango Positive Futures > Spot Normal markets, storage costs
Backwardation Negative Futures < Spot Supply shortages, high demand

Cost of Carry Model

In theory, the futures price should reflect:

Futures Price = Spot Price × (1 + r - y)^t

Where:
- r = risk-free interest rate
- y = convenience yield (benefit of holding physical)
- t = time to expiration
@dataclass
class BasisAnalysis:
    """
    Analysis of futures basis.
    
    Attributes:
        spot_price: Current spot price
        futures_price: Futures contract price
        days_to_expiry: Days until contract expiration
    """
    spot_price: float
    futures_price: float
    days_to_expiry: int
    
    @property
    def basis(self) -> float:
        """Calculate absolute basis."""
        return self.futures_price - self.spot_price
    
    @property
    def basis_percent(self) -> float:
        """Calculate basis as percentage of spot."""
        return (self.basis / self.spot_price) * 100
    
    @property
    def annualized_basis(self) -> float:
        """Annualize the basis percentage."""
        if self.days_to_expiry <= 0:
            return 0
        return self.basis_percent * (365 / self.days_to_expiry)
    
    @property
    def market_condition(self) -> str:
        """Determine market condition."""
        if self.basis > 0:
            return 'Contango'
        elif self.basis < 0:
            return 'Backwardation'
        else:
            return 'Flat'
    
    def implied_yield(self) -> float:
        """Calculate implied yield/cost of carry."""
        if self.days_to_expiry <= 0:
            return 0
        return self.annualized_basis


# Example basis analysis
examples = [
    ('Gold', 2050, 2065, 60),      # Contango
    ('Crude Oil', 75.00, 73.50, 30),  # Backwardation
    ('S&P 500', 5000, 5012, 45),   # Slight contango
]

print("Basis Analysis Examples")
print("=" * 70)

for name, spot, futures, days in examples:
    analysis = BasisAnalysis(spot, futures, days)
    print(f"\n{name}:")
    print(f"  Spot: ${spot:,.2f} | Futures: ${futures:,.2f} | Days: {days}")
    print(f"  Basis: ${analysis.basis:,.2f} ({analysis.basis_percent:.2f}%)")
    print(f"  Annualized: {analysis.annualized_basis:.2f}%")
    print(f"  Market: {analysis.market_condition}")
# Visualize contango and backwardation
def plot_term_structure(contracts: List[Tuple[str, float, int]], title: str):
    """
    Plot futures term structure.
    
    Args:
        contracts: List of (name, price, days_to_expiry) tuples
        title: Chart title
    """
    fig, ax = plt.subplots(figsize=(10, 5))
    
    # Sort by expiry
    contracts = sorted(contracts, key=lambda x: x[2])
    
    names = [c[0] for c in contracts]
    prices = [c[1] for c in contracts]
    days = [c[2] for c in contracts]
    
    # Plot
    ax.plot(days, prices, 'bo-', linewidth=2, markersize=8)
    
    # Add labels
    for i, (name, price, day) in enumerate(contracts):
        ax.annotate(f'{name}\n${price:.2f}', (day, price),
                   textcoords='offset points', xytext=(0, 10),
                   ha='center', fontsize=9)
    
    # Determine structure
    if prices[-1] > prices[0]:
        structure = 'CONTANGO'
        color = 'green'
    else:
        structure = 'BACKWARDATION'
        color = 'red'
    
    ax.text(0.95, 0.95, structure, transform=ax.transAxes,
           fontsize=14, fontweight='bold', color=color,
           ha='right', va='top')
    
    ax.set_xlabel('Days to Expiration')
    ax.set_ylabel('Price')
    ax.set_title(title)
    ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()


# Example: Gold in contango
gold_contracts = [
    ('Spot', 2050, 0),
    ('Feb', 2055, 30),
    ('Apr', 2062, 90),
    ('Jun', 2070, 150),
    ('Aug', 2078, 210),
]
plot_term_structure(gold_contracts, 'Gold Futures Term Structure')
# Example: Oil in backwardation
oil_contracts = [
    ('Spot', 78.00, 0),
    ('Feb', 76.50, 30),
    ('Mar', 75.20, 60),
    ('Apr', 74.10, 90),
    ('May', 73.20, 120),
]
plot_term_structure(oil_contracts, 'Crude Oil Futures Term Structure')

Exercise 2.2: Analyze Basis (Guided)

Complete the function to calculate roll yield and analyze term structure.

Exercise
Solution 2.2
def calculate_roll_yield(
    front_month_price: float,
    back_month_price: float,
    days_between: int
) -> dict:
    """
    Calculate the roll yield when rolling from front to back month.

    Args:
        front_month_price: Price of expiring contract
        back_month_price: Price of next contract
        days_between: Days between contract expirations

    Returns:
        Dictionary with roll yield analysis
    """
    # Calculate raw roll
    roll_difference = back_month_price - front_month_price

    # Calculate roll yield percentage
    roll_yield_pct = (roll_difference / front_month_price) * 100  # Convert to percentage

    # Annualize the roll yield
    annualized_yield = roll_yield_pct * (365 / days_between)  # Days in year

    # Determine if positive or negative roll
    if roll_difference > 0:
        roll_type = 'Negative Roll Yield'  # Contango hurts long holders
        structure = 'Contango'  # Market structure name
    else:
        roll_type = 'Positive Roll Yield'  # Backwardation helps long holders
        structure = 'Backwardation'

    return {
        'front_price': front_month_price,
        'back_price': back_month_price,
        'roll_difference': round(roll_difference, 4),
        'roll_yield_pct': round(roll_yield_pct, 2),
        'annualized_yield': round(annualized_yield, 2),
        'roll_type': roll_type,
        'structure': structure
    }

2.4 Contract Months & Rollover

Contract Month Codes

Futures contracts use single-letter codes for expiration months:

Code Month Code Month
F January N July
G February Q August
H March U September
J April V October
K May X November
M June Z December

Contract Symbol Format

ESH24 = E-mini S&P 500, March 2024
  │└┴── Year (24 = 2024)
  └──── Month (H = March)
# Contract month codes
MONTH_CODES = {
    'F': (1, 'January'),
    'G': (2, 'February'),
    'H': (3, 'March'),
    'J': (4, 'April'),
    'K': (5, 'May'),
    'M': (6, 'June'),
    'N': (7, 'July'),
    'Q': (8, 'August'),
    'U': (9, 'September'),
    'V': (10, 'October'),
    'X': (11, 'November'),
    'Z': (12, 'December')
}

# Reverse lookup
MONTH_TO_CODE = {v[0]: k for k, v in MONTH_CODES.items()}


class FuturesSymbolParser:
    """
    Parser for futures contract symbols.
    
    Handles symbols like ESH24, CLZ25, etc.
    """
    
    def __init__(self, symbol: str):
        self.raw_symbol = symbol.upper()
        self._parse()
    
    def _parse(self):
        """Parse the symbol into components."""
        # Assume format: ROOT + MONTH_CODE + YEAR (2 digits)
        # e.g., ESH24, CLZ25, GCM24
        
        # Find the month code (second to last character before year)
        if len(self.raw_symbol) >= 4:
            self.year_str = self.raw_symbol[-2:]
            self.month_code = self.raw_symbol[-3]
            self.root = self.raw_symbol[:-3]
        else:
            raise ValueError(f"Invalid symbol format: {self.raw_symbol}")
    
    @property
    def year(self) -> int:
        """Get full year."""
        year_2digit = int(self.year_str)
        return 2000 + year_2digit if year_2digit < 50 else 1900 + year_2digit
    
    @property
    def month(self) -> int:
        """Get month number."""
        return MONTH_CODES[self.month_code][0]
    
    @property
    def month_name(self) -> str:
        """Get month name."""
        return MONTH_CODES[self.month_code][1]
    
    @property
    def expiration_date(self) -> datetime:
        """Estimate expiration date (third Friday of month)."""
        # Find third Friday
        first_day = datetime(self.year, self.month, 1)
        # Days until Friday (4 = Friday)
        days_until_friday = (4 - first_day.weekday()) % 7
        first_friday = first_day + timedelta(days=days_until_friday)
        third_friday = first_friday + timedelta(weeks=2)
        return third_friday
    
    def days_to_expiry(self, as_of: datetime = None) -> int:
        """Calculate days until expiration."""
        if as_of is None:
            as_of = datetime.now()
        delta = self.expiration_date - as_of
        return max(0, delta.days)
    
    def __repr__(self) -> str:
        return f"{self.root} {self.month_name} {self.year}"


# Test the parser
symbols = ['ESH25', 'CLZ25', 'GCM25', 'NQU25', '6EH25']

print("Futures Symbol Parsing")
print("=" * 60)
for sym in symbols:
    parser = FuturesSymbolParser(sym)
    print(f"{sym:8} -> {parser!r:25} (Expires: {parser.expiration_date.strftime('%Y-%m-%d')})")
class ContractRollover:
    """
    Handles futures contract rollover logic.
    
    Attributes:
        root_symbol: Base contract symbol (e.g., 'ES')
        contract_months: List of valid contract month codes
    """
    
    def __init__(self, root_symbol: str, contract_months: List[str]):
        self.root_symbol = root_symbol.upper()
        self.contract_months = [m.upper() for m in contract_months]
    
    def get_contract_symbol(self, month_code: str, year: int) -> str:
        """Generate full contract symbol."""
        year_str = str(year)[-2:]
        return f"{self.root_symbol}{month_code}{year_str}"
    
    def get_active_contract(self, as_of: datetime = None) -> str:
        """
        Get the currently active (front month) contract.
        
        Args:
            as_of: Reference date (default: today)
            
        Returns:
            Active contract symbol
        """
        if as_of is None:
            as_of = datetime.now()
        
        current_month = as_of.month
        current_year = as_of.year
        
        # Find the next valid contract month
        for month_code in self.contract_months:
            month_num = MONTH_CODES[month_code][0]
            
            if month_num > current_month:
                return self.get_contract_symbol(month_code, current_year)
            elif month_num == current_month:
                # Check if we're past typical roll date (e.g., 2nd Thursday)
                if as_of.day > 15:  # Simplified: roll after 15th
                    continue
                return self.get_contract_symbol(month_code, current_year)
        
        # Roll to next year's first contract
        return self.get_contract_symbol(self.contract_months[0], current_year + 1)
    
    def get_contract_chain(self, num_contracts: int = 4, as_of: datetime = None) -> List[str]:
        """
        Get a chain of upcoming contracts.
        
        Args:
            num_contracts: Number of contracts to return
            as_of: Reference date
            
        Returns:
            List of contract symbols
        """
        if as_of is None:
            as_of = datetime.now()
        
        contracts = []
        current_month = as_of.month
        current_year = as_of.year
        
        # Build list of all possible contracts for next 2 years
        all_contracts = []
        for year in [current_year, current_year + 1]:
            for month_code in self.contract_months:
                month_num = MONTH_CODES[month_code][0]
                if year == current_year and month_num < current_month:
                    continue
                all_contracts.append(self.get_contract_symbol(month_code, year))
        
        return all_contracts[:num_contracts]


# Test rollover logic
es_rollover = ContractRollover('ES', ['H', 'M', 'U', 'Z'])

print("E-mini S&P 500 Contract Chain")
print("=" * 40)
print(f"Active Contract: {es_rollover.get_active_contract()}")
print(f"\nUpcoming Contracts:")
for contract in es_rollover.get_contract_chain(4):
    parser = FuturesSymbolParser(contract)
    print(f"  {contract} -> {parser.month_name} {parser.year}")

Exercise 2.3: Handle Rollover (Guided)

Complete the function to build a continuous futures price series by handling rollovers.

Exercise
Solution 2.3
def build_continuous_series(
    contract_data: Dict[str, pd.DataFrame],
    roll_dates: Dict[str, datetime],
    adjustment_method: str = 'ratio'
) -> pd.DataFrame:
    """
    Build a continuous futures price series from individual contracts.

    Args:
        contract_data: Dict of contract symbol -> price DataFrame
        roll_dates: Dict of contract symbol -> roll date
        adjustment_method: 'ratio' or 'difference'

    Returns:
        Continuous price series
    """
    # Sort contracts by roll date
    sorted_contracts = sorted(roll_dates.items(), key=lambda x: x[1])

    continuous = pd.DataFrame()
    adjustment_factor = 1.0 if adjustment_method == 'ratio' else 0.0

    for i, (contract, roll_date) in enumerate(sorted_contracts):
        if contract not in contract_data:
            continue

        df = contract_data[contract].copy()

        # Get data up to roll date
        if i < len(sorted_contracts) - 1:
            next_contract = sorted_contracts[i + 1][0]
            next_roll = sorted_contracts[i + 1][1]
            mask = df.index < roll_date  # Filter by index (date)
            df = df[mask]

            # Calculate adjustment at roll point
            if next_contract in contract_data:
                next_df = contract_data[next_contract]

                # Find prices at roll date
                try:
                    old_price = df['close'].iloc[-1]
                    # Get first price from next contract after roll
                    next_prices = next_df[next_df.index >= roll_date]
                    if len(next_prices) > 0:
                        new_price = next_prices['close'].iloc[0]

                        if adjustment_method == 'ratio':
                            adjustment_factor *= new_price / old_price  # Divide by old price
                        else:
                            adjustment_factor += new_price - old_price
                except:
                    pass

        # Apply adjustment
        if adjustment_method == 'ratio':  # Check method type
            df['adjusted_close'] = df['close'] * adjustment_factor
        else:
            df['adjusted_close'] = df['close'] + adjustment_factor

        continuous = pd.concat([continuous, df[['close', 'adjusted_close']]])

    return continuous.sort_index()

Exercise 2.4: Futures Contract Analyzer (Open-ended)

Build a comprehensive futures contract analyzer that calculates margin requirements, leverage, and risk metrics.

Your implementation:

Exercise
Solution 2.4
class FuturesAnalyzer:
    """
    Comprehensive analyzer for futures contracts.

    Attributes:
        contract: FuturesContract specification
        current_price: Current contract price
    """

    def __init__(self, symbol: str, current_price: float):
        self.contract = FUTURES_CONTRACTS.get(symbol.upper())
        if self.contract is None:
            raise ValueError(f"Unknown contract: {symbol}")
        self.current_price = current_price

    def get_notional_value(self) -> float:
        """Calculate the notional value of one contract."""
        return self.contract.contract_size * self.current_price

    def get_leverage(self, margin: float) -> float:
        """Calculate leverage based on margin requirement."""
        return self.get_notional_value() / margin

    def position_size_by_risk(
        self,
        account: float,
        risk_pct: float,
        stop_ticks: int
    ) -> int:
        """Calculate position size based on risk parameters."""
        risk_amount = account * (risk_pct / 100)
        risk_per_contract = stop_ticks * self.contract.tick_value
        return max(1, int(risk_amount / risk_per_contract))

    def scenario_analysis(self, price_changes: List[float]) -> pd.DataFrame:
        """Analyze P&L under different price scenarios."""
        results = []
        for change_pct in price_changes:
            new_price = self.current_price * (1 + change_pct / 100)
            points_change = new_price - self.current_price
            pnl = points_change * self.contract.point_value

            results.append({
                'Price Change (%)': f"{change_pct:+.1f}%",
                'New Price': round(new_price, 2),
                'Points': round(points_change, 2),
                'P&L (1 contract)': f"${pnl:+,.2f}"
            })
        return pd.DataFrame(results)

    def summary(self) -> dict:
        """Get comprehensive contract summary."""
        return {
            'symbol': self.contract.symbol,
            'name': self.contract.name,
            'current_price': self.current_price,
            'notional_value': f"${self.get_notional_value():,.2f}",
            'point_value': f"${self.contract.point_value:.2f}",
            'tick_size': self.contract.tick_size,
            'tick_value': f"${self.contract.tick_value:.2f}"
        }


# Test the analyzer
analyzer = FuturesAnalyzer('ES', 5000)

print("E-mini S&P 500 Analysis")
print("=" * 50)
for k, v in analyzer.summary().items():
    print(f"{k}: {v}")

print(f"\nLeverage at $12,000 margin: {analyzer.get_leverage(12000):.1f}:1")
print(f"Position size ($50k account, 2% risk, 20 tick stop): "
      f"{analyzer.position_size_by_risk(50000, 2, 20)} contracts")

print("\nScenario Analysis:")
print(analyzer.scenario_analysis([-5, -2, -1, 0, 1, 2, 5]).to_string(index=False))

Exercise 2.5: Term Structure Analyzer (Open-ended)

Build a term structure analyzer that tracks the futures curve and identifies trading opportunities.

Your implementation:

Exercise
Solution 2.5
class TermStructureAnalyzer:
    """
    Analyzes futures term structure for trading opportunities.

    Attributes:
        underlying: Name of the underlying asset
        contracts: List of contract data
    """

    def __init__(self, underlying: str):
        self.underlying = underlying
        self.contracts = []

    def add_contract(self, symbol: str, price: float, expiry: datetime):
        """Add a contract to the term structure."""
        days_to_expiry = (expiry - datetime.now()).days
        self.contracts.append({
            'symbol': symbol,
            'price': price,
            'expiry': expiry,
            'days_to_expiry': max(0, days_to_expiry)
        })
        # Sort by expiry
        self.contracts.sort(key=lambda x: x['expiry'])

    def get_structure(self) -> str:
        """Determine overall term structure."""
        if len(self.contracts) < 2:
            return 'Unknown'

        slopes = []
        for i in range(len(self.contracts) - 1):
            slope = self.contracts[i+1]['price'] - self.contracts[i]['price']
            slopes.append(slope)

        positive = sum(1 for s in slopes if s > 0)
        negative = sum(1 for s in slopes if s < 0)

        if positive == len(slopes):
            return 'Contango'
        elif negative == len(slopes):
            return 'Backwardation'
        else:
            return 'Mixed'

    def calculate_slope(self) -> float:
        """Calculate annualized term structure slope."""
        if len(self.contracts) < 2:
            return 0.0

        front = self.contracts[0]
        back = self.contracts[-1]

        price_diff = back['price'] - front['price']
        days_diff = back['days_to_expiry'] - front['days_to_expiry']

        if days_diff <= 0:
            return 0.0

        # Annualized percentage
        return (price_diff / front['price']) * (365 / days_diff) * 100

    def find_spread_opportunities(self) -> List[dict]:
        """Find calendar spread opportunities."""
        opportunities = []

        for i in range(len(self.contracts) - 1):
            front = self.contracts[i]
            back = self.contracts[i + 1]

            spread = back['price'] - front['price']
            days_diff = back['days_to_expiry'] - front['days_to_expiry']

            if days_diff > 0:
                annualized = (spread / front['price']) * (365 / days_diff) * 100
            else:
                annualized = 0

            opportunities.append({
                'front': front['symbol'],
                'back': back['symbol'],
                'spread': round(spread, 4),
                'spread_pct': round(spread / front['price'] * 100, 2),
                'annualized_pct': round(annualized, 2),
                'days_between': days_diff
            })

        return opportunities

    def plot_term_structure(self):
        """Visualize the term structure."""
        if not self.contracts:
            print("No contracts to plot")
            return

        fig, ax = plt.subplots(figsize=(10, 5))

        days = [c['days_to_expiry'] for c in self.contracts]
        prices = [c['price'] for c in self.contracts]
        symbols = [c['symbol'] for c in self.contracts]

        ax.plot(days, prices, 'bo-', linewidth=2, markersize=10)

        for i, (d, p, s) in enumerate(zip(days, prices, symbols)):
            ax.annotate(f'{s}\n${p:.2f}', (d, p),
                       textcoords='offset points', xytext=(0, 12),
                       ha='center', fontsize=9)

        structure = self.get_structure()
        color = 'green' if structure == 'Contango' else 'red' if structure == 'Backwardation' else 'orange'
        ax.text(0.95, 0.95, structure, transform=ax.transAxes,
               fontsize=14, fontweight='bold', color=color,
               ha='right', va='top')

        ax.set_xlabel('Days to Expiration')
        ax.set_ylabel('Price')
        ax.set_title(f'{self.underlying} Futures Term Structure')
        ax.grid(True, alpha=0.3)

        plt.tight_layout()
        plt.show()


# Test the analyzer
analyzer = TermStructureAnalyzer('Crude Oil')

# Add contracts (backwardation example)
analyzer.add_contract('CLG25', 78.50, datetime(2025, 2, 20))
analyzer.add_contract('CLH25', 77.20, datetime(2025, 3, 20))
analyzer.add_contract('CLJ25', 76.10, datetime(2025, 4, 22))
analyzer.add_contract('CLK25', 75.20, datetime(2025, 5, 20))

print(f"Structure: {analyzer.get_structure()}")
print(f"Annualized Slope: {analyzer.calculate_slope():.2f}%")

print("\nSpread Opportunities:")
for opp in analyzer.find_spread_opportunities():
    print(f"  {opp['front']}/{opp['back']}: ${opp['spread']:.2f} "
          f"({opp['annualized_pct']:.1f}% ann.)")

analyzer.plot_term_structure()

Exercise 2.6: Futures Data Handler (Open-ended)

Build a comprehensive data handler that manages multiple futures contracts and their data.

Your implementation:

Exercise
Solution 2.6
class FuturesDataHandler:
    """
    Comprehensive handler for futures contract data.

    Manages multiple contracts, handles rollovers, and provides
    analysis capabilities for futures data.
    """

    def __init__(self, root_symbol: str, contract_months: List[str]):
        self.root_symbol = root_symbol
        self.contract_months = contract_months
        self.contract_data = {}
        self.roll_dates = {}

    def add_contract_data(self, symbol: str, data: pd.DataFrame):
        """Add price data for a contract."""
        symbol = symbol.upper()
        self.contract_data[symbol] = data.copy()

        # Parse expiration and set roll date
        parser = FuturesSymbolParser(symbol)
        # Roll 5 days before expiration
        roll_date = parser.expiration_date - timedelta(days=5)
        self.roll_dates[symbol] = roll_date

    def get_front_month(self, as_of: datetime = None) -> str:
        """Get the front month contract symbol."""
        if as_of is None:
            as_of = datetime.now()

        # Find contracts that haven't expired yet
        active = []
        for symbol, roll_date in self.roll_dates.items():
            if roll_date > as_of:
                parser = FuturesSymbolParser(symbol)
                active.append((symbol, parser.expiration_date))

        if not active:
            return None

        # Return the nearest expiration
        active.sort(key=lambda x: x[1])
        return active[0][0]

    def build_continuous(self, method: str = 'ratio') -> pd.DataFrame:
        """Build continuous price series."""
        return build_continuous_series(
            self.contract_data,
            self.roll_dates,
            method
        )

    def get_term_structure(self, as_of: datetime = None) -> pd.DataFrame:
        """Get term structure as of a date."""
        if as_of is None:
            as_of = datetime.now()

        results = []
        for symbol, data in self.contract_data.items():
            parser = FuturesSymbolParser(symbol)

            # Get price as of date
            if isinstance(data.index, pd.DatetimeIndex):
                mask = data.index <= as_of
                if mask.any():
                    price = data.loc[mask, 'close'].iloc[-1]
                else:
                    continue
            else:
                price = data['close'].iloc[-1]

            days_to_expiry = (parser.expiration_date - as_of).days

            results.append({
                'symbol': symbol,
                'month': parser.month_name,
                'year': parser.year,
                'price': price,
                'days_to_expiry': days_to_expiry
            })

        df = pd.DataFrame(results)
        return df.sort_values('days_to_expiry')

    def calculate_historical_basis(
        self,
        spot_data: pd.Series
    ) -> pd.DataFrame:
        """Calculate historical basis vs spot."""
        continuous = self.build_continuous('ratio')

        # Align dates
        combined = pd.DataFrame({
            'futures': continuous['adjusted_close'],
            'spot': spot_data
        }).dropna()

        combined['basis'] = combined['futures'] - combined['spot']
        combined['basis_pct'] = (combined['basis'] / combined['spot']) * 100

        return combined

    def summary(self) -> dict:
        """Get handler summary."""
        return {
            'root_symbol': self.root_symbol,
            'contracts_loaded': len(self.contract_data),
            'contract_symbols': list(self.contract_data.keys()),
            'front_month': self.get_front_month()
        }


# Test the handler
handler = FuturesDataHandler('ES', ['H', 'M', 'U', 'Z'])

# Add mock data
for symbol, df in contract_data.items():
    handler.add_contract_data(symbol, df)

print("Futures Data Handler Summary")
print("=" * 40)
for k, v in handler.summary().items():
    print(f"{k}: {v}")

print("\nTerm Structure:")
print(handler.get_term_structure(datetime(2024, 2, 1)))

Module Project: Futures Data Handler

Build a comprehensive futures data handler that combines all concepts from this module.

class ComprehensiveFuturesHandler:
    """
    Complete futures data and analysis system.
    
    Combines contract specifications, price data management,
    rollover handling, and term structure analysis.
    """
    
    def __init__(self, root_symbol: str):
        """
        Initialize the handler.
        
        Args:
            root_symbol: Base symbol (e.g., 'ES', 'CL')
        """
        self.root_symbol = root_symbol.upper()
        self.contract_spec = FUTURES_CONTRACTS.get(self.root_symbol)
        
        self.contracts = {}  # symbol -> {'data': df, 'expiry': datetime}
        self.spot_data = None
        self.continuous_data = None
    
    # ==================== Contract Management ====================
    
    def add_contract(
        self,
        symbol: str,
        data: pd.DataFrame,
        expiry: datetime = None
    ):
        """
        Add a contract with its price data.
        
        Args:
            symbol: Full contract symbol (e.g., 'ESH25')
            data: OHLCV DataFrame
            expiry: Expiration date (auto-calculated if None)
        """
        symbol = symbol.upper()
        
        if expiry is None:
            parser = FuturesSymbolParser(symbol)
            expiry = parser.expiration_date
        
        self.contracts[symbol] = {
            'data': data.copy(),
            'expiry': expiry,
            'roll_date': expiry - timedelta(days=5)
        }
    
    def set_spot_data(self, data: pd.Series):
        """Set spot price data for basis calculations."""
        self.spot_data = data.copy()
    
    def get_active_contracts(self, as_of: datetime = None) -> List[str]:
        """Get list of active (non-expired) contracts."""
        if as_of is None:
            as_of = datetime.now()
        
        active = [
            symbol for symbol, info in self.contracts.items()
            if info['expiry'] > as_of
        ]
        
        # Sort by expiry
        active.sort(key=lambda s: self.contracts[s]['expiry'])
        return active
    
    def get_front_month(self, as_of: datetime = None) -> Optional[str]:
        """Get the front month contract."""
        active = self.get_active_contracts(as_of)
        return active[0] if active else None
    
    # ==================== Continuous Contract ====================
    
    def build_continuous(
        self,
        method: str = 'ratio',
        roll_days_before: int = 5
    ) -> pd.DataFrame:
        """
        Build continuous price series.
        
        Args:
            method: Adjustment method ('ratio' or 'difference')
            roll_days_before: Days before expiry to roll
            
        Returns:
            Continuous price DataFrame
        """
        if not self.contracts:
            return pd.DataFrame()
        
        # Sort contracts by expiry
        sorted_contracts = sorted(
            self.contracts.items(),
            key=lambda x: x[1]['expiry']
        )
        
        continuous = pd.DataFrame()
        adjustment = 1.0 if method == 'ratio' else 0.0
        
        for i, (symbol, info) in enumerate(sorted_contracts):
            df = info['data'].copy()
            roll_date = info['roll_date']
            
            # Filter data up to roll date
            if i < len(sorted_contracts) - 1:
                df = df[df.index < roll_date]
                
                # Calculate adjustment
                if len(df) > 0:
                    next_symbol = sorted_contracts[i + 1][0]
                    next_data = self.contracts[next_symbol]['data']
                    
                    try:
                        old_price = df['close'].iloc[-1]
                        new_data = next_data[next_data.index >= roll_date]
                        if len(new_data) > 0:
                            new_price = new_data['close'].iloc[0]
                            
                            if method == 'ratio':
                                adjustment *= new_price / old_price
                            else:
                                adjustment += new_price - old_price
                    except Exception:
                        pass
            
            # Apply adjustment
            df['contract'] = symbol
            if method == 'ratio':
                df['adjusted'] = df['close'] * adjustment
            else:
                df['adjusted'] = df['close'] + adjustment
            
            continuous = pd.concat([continuous, df])
        
        self.continuous_data = continuous.sort_index()
        return self.continuous_data
    
    # ==================== Term Structure ====================
    
    def get_term_structure(self, as_of: datetime = None) -> pd.DataFrame:
        """
        Get term structure snapshot.
        
        Args:
            as_of: Reference date
            
        Returns:
            DataFrame with term structure
        """
        if as_of is None:
            as_of = datetime.now()
        
        results = []
        for symbol, info in self.contracts.items():
            data = info['data']
            expiry = info['expiry']
            
            # Get price as of date
            mask = data.index <= as_of
            if not mask.any():
                continue
            
            price = data.loc[mask, 'close'].iloc[-1]
            days_to_expiry = (expiry - as_of).days
            
            results.append({
                'symbol': symbol,
                'price': price,
                'expiry': expiry.strftime('%Y-%m-%d'),
                'days_to_expiry': days_to_expiry
            })
        
        df = pd.DataFrame(results)
        if not df.empty:
            df = df.sort_values('days_to_expiry')
        return df
    
    def analyze_term_structure(self, as_of: datetime = None) -> dict:
        """
        Analyze current term structure.
        
        Returns:
            Analysis dictionary
        """
        ts = self.get_term_structure(as_of)
        
        if len(ts) < 2:
            return {'structure': 'Unknown', 'error': 'Insufficient data'}
        
        # Calculate price changes
        prices = ts['price'].values
        changes = np.diff(prices)
        
        # Determine structure
        if all(c > 0 for c in changes):
            structure = 'Contango'
        elif all(c < 0 for c in changes):
            structure = 'Backwardation'
        else:
            structure = 'Mixed'
        
        # Calculate front-to-back spread
        front_price = ts.iloc[0]['price']
        back_price = ts.iloc[-1]['price']
        total_spread = back_price - front_price
        spread_pct = (total_spread / front_price) * 100
        
        # Annualize
        days_span = ts.iloc[-1]['days_to_expiry'] - ts.iloc[0]['days_to_expiry']
        if days_span > 0:
            annualized = spread_pct * (365 / days_span)
        else:
            annualized = 0
        
        return {
            'structure': structure,
            'front_month': ts.iloc[0]['symbol'],
            'back_month': ts.iloc[-1]['symbol'],
            'total_spread': round(total_spread, 4),
            'spread_pct': round(spread_pct, 2),
            'annualized_pct': round(annualized, 2),
            'num_contracts': len(ts)
        }
    
    # ==================== Basis Analysis ====================
    
    def calculate_basis(self, as_of: datetime = None) -> Optional[dict]:
        """
        Calculate current basis vs spot.
        
        Returns:
            Basis analysis dictionary
        """
        if self.spot_data is None:
            return None
        
        if as_of is None:
            as_of = datetime.now()
        
        # Get spot price
        spot_mask = self.spot_data.index <= as_of
        if not spot_mask.any():
            return None
        spot_price = self.spot_data[spot_mask].iloc[-1]
        
        # Get front month futures price
        front = self.get_front_month(as_of)
        if front is None:
            return None
        
        futures_data = self.contracts[front]['data']
        futures_mask = futures_data.index <= as_of
        if not futures_mask.any():
            return None
        futures_price = futures_data.loc[futures_mask, 'close'].iloc[-1]
        
        # Calculate basis
        basis = futures_price - spot_price
        basis_pct = (basis / spot_price) * 100
        
        # Days to expiry
        days_to_expiry = (self.contracts[front]['expiry'] - as_of).days
        
        # Annualize
        if days_to_expiry > 0:
            annualized = basis_pct * (365 / days_to_expiry)
        else:
            annualized = 0
        
        return {
            'spot_price': round(spot_price, 4),
            'futures_price': round(futures_price, 4),
            'front_month': front,
            'basis': round(basis, 4),
            'basis_pct': round(basis_pct, 2),
            'annualized_pct': round(annualized, 2),
            'days_to_expiry': days_to_expiry,
            'condition': 'Contango' if basis > 0 else 'Backwardation' if basis < 0 else 'Flat'
        }
    
    # ==================== Visualization ====================
    
    def plot_term_structure(self, as_of: datetime = None):
        """Plot the current term structure."""
        ts = self.get_term_structure(as_of)
        if ts.empty:
            print("No data to plot")
            return
        
        analysis = self.analyze_term_structure(as_of)
        
        fig, ax = plt.subplots(figsize=(10, 5))
        
        ax.plot(ts['days_to_expiry'], ts['price'], 'bo-', linewidth=2, markersize=10)
        
        for _, row in ts.iterrows():
            ax.annotate(
                f"{row['symbol']}\n${row['price']:.2f}",
                (row['days_to_expiry'], row['price']),
                textcoords='offset points',
                xytext=(0, 12),
                ha='center',
                fontsize=9
            )
        
        # Add structure label
        color = 'green' if analysis['structure'] == 'Contango' else 'red'
        ax.text(
            0.95, 0.95,
            f"{analysis['structure']}\n{analysis['annualized_pct']:.1f}% ann.",
            transform=ax.transAxes,
            fontsize=12,
            fontweight='bold',
            color=color,
            ha='right',
            va='top'
        )
        
        ax.set_xlabel('Days to Expiration')
        ax.set_ylabel('Price')
        ax.set_title(f'{self.root_symbol} Futures Term Structure')
        ax.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.show()
    
    def plot_continuous(self):
        """Plot continuous price series."""
        if self.continuous_data is None:
            self.build_continuous()
        
        if self.continuous_data is None or self.continuous_data.empty:
            print("No continuous data available")
            return
        
        fig, axes = plt.subplots(2, 1, figsize=(12, 8), height_ratios=[3, 1])
        
        # Price chart
        ax1 = axes[0]
        ax1.plot(self.continuous_data.index, self.continuous_data['adjusted'],
                label='Adjusted', linewidth=1.5)
        ax1.plot(self.continuous_data.index, self.continuous_data['close'],
                label='Unadjusted', linewidth=1, alpha=0.5)
        
        # Mark roll dates
        for symbol, info in self.contracts.items():
            roll_date = info['roll_date']
            if roll_date in self.continuous_data.index or \
               any(self.continuous_data.index < roll_date):
                ax1.axvline(roll_date, color='red', linestyle='--', alpha=0.5)
        
        ax1.set_ylabel('Price')
        ax1.set_title(f'{self.root_symbol} Continuous Futures')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # Contract chart
        ax2 = axes[1]
        contracts = self.continuous_data['contract'].unique()
        colors = plt.cm.Set1(np.linspace(0, 1, len(contracts)))
        
        for contract, color in zip(contracts, colors):
            mask = self.continuous_data['contract'] == contract
            data = self.continuous_data[mask]
            ax2.fill_between(data.index, 0, 1, alpha=0.7, label=contract, color=color)
        
        ax2.set_ylabel('Contract')
        ax2.set_xlabel('Date')
        ax2.legend(loc='upper left', ncol=len(contracts))
        ax2.set_yticks([])
        
        plt.tight_layout()
        plt.show()
    
    # ==================== Summary ====================
    
    def summary(self) -> dict:
        """Get comprehensive handler summary."""
        return {
            'root_symbol': self.root_symbol,
            'contract_spec': self.contract_spec.name if self.contract_spec else 'Unknown',
            'contracts_loaded': len(self.contracts),
            'contract_symbols': list(self.contracts.keys()),
            'front_month': self.get_front_month(),
            'has_spot_data': self.spot_data is not None,
            'has_continuous': self.continuous_data is not None
        }
    
    def print_summary(self):
        """Print detailed summary."""
        print("\n" + "=" * 60)
        print(f"FUTURES DATA HANDLER: {self.root_symbol}")
        print("=" * 60)
        
        summary = self.summary()
        print(f"\nContract: {summary['contract_spec']}")
        print(f"Contracts Loaded: {summary['contracts_loaded']}")
        print(f"Front Month: {summary['front_month']}")
        
        if self.contracts:
            print("\n" + "-" * 60)
            print("TERM STRUCTURE")
            print("-" * 60)
            ts = self.get_term_structure()
            print(ts.to_string(index=False))
            
            analysis = self.analyze_term_structure()
            print(f"\nStructure: {analysis['structure']}")
            print(f"Spread: {analysis['spread_pct']:.2f}% ({analysis['annualized_pct']:.2f}% ann.)")
        
        print("\n" + "=" * 60)
# Demonstrate the Comprehensive Futures Handler

# Create handler for E-mini S&P 500
handler = ComprehensiveFuturesHandler('ES')

# Generate mock data for multiple contracts
np.random.seed(42)
base_price = 5000

contracts_to_add = [
    ('ESH25', datetime(2025, 3, 21), 0),
    ('ESM25', datetime(2025, 6, 20), 5),   # Slight contango
    ('ESU25', datetime(2025, 9, 19), 10),
    ('ESZ25', datetime(2025, 12, 19), 15),
]

for symbol, expiry, premium in contracts_to_add:
    # Generate 90 days of data
    dates = pd.date_range(end=datetime.now(), periods=90, freq='D')
    prices = base_price + premium + np.cumsum(np.random.randn(90) * 15)
    
    data = pd.DataFrame({
        'open': prices - np.random.uniform(2, 5, 90),
        'high': prices + np.random.uniform(5, 15, 90),
        'low': prices - np.random.uniform(5, 15, 90),
        'close': prices,
        'volume': np.random.randint(100000, 500000, 90)
    }, index=dates)
    
    handler.add_contract(symbol, data, expiry)

# Print summary
handler.print_summary()
# Visualize term structure
handler.plot_term_structure()
# Build and visualize continuous contract
continuous = handler.build_continuous('ratio')
print("Continuous Series Sample:")
print(continuous.tail(10)[['close', 'adjusted', 'contract']])

Key Takeaways

  • Futures contracts are standardized agreements with specific tick sizes, values, and expiration dates
  • Contract specifications define the multiplier, tick value, and point value that determine P&L
  • Popular markets include equity indices (ES, NQ), energy (CL, NG), metals (GC, SI), and currencies (6E, 6J)
  • Basis is the difference between futures and spot prices; positive = contango, negative = backwardation
  • Term structure shows how prices vary across expiration dates and impacts roll yield
  • Contract months use letter codes (H=March, M=June, U=September, Z=December for quarterlies)
  • Rollover requires adjusting for price gaps when switching contracts (ratio or difference method)
  • Continuous contracts enable backtesting but require careful handling of roll adjustments

Next: Module 3 - Leverage & Margin

Module 3: Leverage & Margin

Part 1: Market Fundamentals

Duration Exercises
~2.5 hours 6

Learning Objectives

By the end of this module, you will be able to:

  • Understand how leverage works in forex and futures markets
  • Calculate margin requirements and leverage ratios
  • Implement proper position sizing with leverage
  • Manage margin calls and liquidation risk
  • Build risk management systems for leveraged trading
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
from enum import Enum

# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')

3.1 Understanding Leverage

Leverage allows you to control a large position with a small amount of capital. It's a double-edged sword that amplifies both gains and losses.

Leverage Formula

Leverage Ratio = Position Value / Margin Required

Example: 50:1 leverage
- Position Value: $100,000
- Margin Required: $2,000 (2%)
- Leverage: 100,000 / 2,000 = 50:1

Leverage by Market

Market Typical Leverage Margin %
Forex (Retail) 50:1 to 500:1 0.2% - 2%
Forex (US Regulated) 50:1 max 2%
Futures 10:1 to 20:1 5% - 10%
Stocks (US) 2:1 to 4:1 25% - 50%
Crypto 2:1 to 125:1 0.8% - 50%
@dataclass
class LeverageCalculator:
    """
    Calculator for leverage and margin.
    
    Attributes:
        leverage_ratio: The leverage ratio (e.g., 50 for 50:1)
    """
    leverage_ratio: float
    
    @property
    def margin_percent(self) -> float:
        """Calculate margin percentage."""
        return 100 / self.leverage_ratio
    
    def margin_required(self, position_value: float) -> float:
        """Calculate required margin for a position."""
        return position_value / self.leverage_ratio
    
    def max_position(self, available_margin: float) -> float:
        """Calculate maximum position size given margin."""
        return available_margin * self.leverage_ratio
    
    def leveraged_return(
        self, 
        price_change_pct: float,
        account_pct_at_risk: float = 100
    ) -> float:
        """
        Calculate leveraged return on account.
        
        Args:
            price_change_pct: Underlying price change percentage
            account_pct_at_risk: Percentage of account used as margin
            
        Returns:
            Account return percentage
        """
        position_return = price_change_pct * self.leverage_ratio
        return position_return * (account_pct_at_risk / 100)
    
    def price_move_to_wipeout(self) -> float:
        """Calculate adverse price move needed to lose all margin."""
        return 100 / self.leverage_ratio


# Compare leverage levels
leverage_levels = [10, 25, 50, 100, 200]

print("Leverage Comparison")
print("=" * 70)
print(f"{'Leverage':<12} {'Margin %':<12} {'$100k Position':<18} {'Wipeout Move'}")
print("-" * 70)

for lev in leverage_levels:
    calc = LeverageCalculator(lev)
    margin = calc.margin_required(100_000)
    wipeout = calc.price_move_to_wipeout()
    print(f"{lev}:1{'':<8} {calc.margin_percent:.1f}%{'':<8} ${margin:,.0f}{'':<12} {wipeout:.1f}%")
# Visualize leverage impact
def plot_leverage_impact():
    """Visualize how leverage amplifies returns."""
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    
    # Price changes from -5% to +5%
    price_changes = np.linspace(-5, 5, 100)
    leverage_levels = [1, 10, 50, 100]
    colors = ['green', 'blue', 'orange', 'red']
    
    # Left plot: Returns at different leverage
    ax1 = axes[0]
    for lev, color in zip(leverage_levels, colors):
        calc = LeverageCalculator(lev)
        returns = [calc.leveraged_return(pc) for pc in price_changes]
        ax1.plot(price_changes, returns, label=f'{lev}:1', color=color, linewidth=2)
    
    ax1.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
    ax1.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
    ax1.axhline(y=-100, color='red', linestyle='--', linewidth=1, alpha=0.7, label='Margin Call')
    ax1.set_xlabel('Price Change (%)')
    ax1.set_ylabel('Account Return (%)')
    ax1.set_title('Leverage Impact on Returns')
    ax1.legend()
    ax1.set_ylim(-150, 150)
    ax1.grid(True, alpha=0.3)
    
    # Right plot: Margin call threshold
    ax2 = axes[1]
    leverage_range = np.arange(1, 201)
    wipeout_moves = [LeverageCalculator(l).price_move_to_wipeout() for l in leverage_range]
    
    ax2.plot(leverage_range, wipeout_moves, color='red', linewidth=2)
    ax2.fill_between(leverage_range, wipeout_moves, 0, alpha=0.2, color='red')
    
    # Add annotations for common leverage levels
    annotations = [(50, 2), (100, 1), (200, 0.5)]
    for lev, move in annotations:
        ax2.annotate(
            f'{lev}:1 = {move}%',
            xy=(lev, move),
            xytext=(lev + 20, move + 3),
            arrowprops=dict(arrowstyle='->', color='black'),
            fontsize=10
        )
    
    ax2.set_xlabel('Leverage Ratio')
    ax2.set_ylabel('Adverse Move to Lose All Margin (%)')
    ax2.set_title('Margin Call Threshold by Leverage')
    ax2.grid(True, alpha=0.3)
    ax2.set_ylim(0, 15)
    
    plt.tight_layout()
    plt.show()

plot_leverage_impact()

3.2 Margin Requirements

Types of Margin

Margin Type Description Typical Level
Initial Margin Required to open a position 2% - 10%
Maintenance Margin Required to keep position open 50% - 75% of initial
Variation Margin Daily P&L settlement (futures) Based on price move

Margin Call Process

1. Account equity falls below maintenance margin
2. Broker issues margin call
3. Trader must:
   - Deposit additional funds, OR
   - Close positions to reduce exposure
4. If no action: Broker liquidates positions
class MarginAccount:
    """
    Simulates a margin trading account.
    
    Attributes:
        balance: Initial cash balance
        initial_margin_pct: Initial margin requirement
        maintenance_margin_pct: Maintenance margin requirement
    """
    
    def __init__(
        self,
        balance: float,
        initial_margin_pct: float = 2.0,
        maintenance_margin_pct: float = 1.0
    ):
        self.initial_balance = balance
        self.cash = balance
        self.initial_margin_pct = initial_margin_pct
        self.maintenance_margin_pct = maintenance_margin_pct
        
        self.positions = []  # List of open positions
        self.trade_history = []
    
    @property
    def leverage_ratio(self) -> float:
        """Calculate effective leverage ratio."""
        return 100 / self.initial_margin_pct
    
    @property
    def total_position_value(self) -> float:
        """Calculate total value of all positions."""
        return sum(p['current_value'] for p in self.positions)
    
    @property
    def unrealized_pnl(self) -> float:
        """Calculate unrealized P&L."""
        return sum(p['unrealized_pnl'] for p in self.positions)
    
    @property
    def equity(self) -> float:
        """Calculate account equity."""
        return self.cash + self.unrealized_pnl
    
    @property
    def used_margin(self) -> float:
        """Calculate margin used by positions."""
        return sum(p['margin_used'] for p in self.positions)
    
    @property
    def free_margin(self) -> float:
        """Calculate available margin."""
        return self.equity - self.used_margin
    
    @property
    def margin_level(self) -> float:
        """Calculate margin level percentage."""
        if self.used_margin == 0:
            return float('inf')
        return (self.equity / self.used_margin) * 100
    
    def open_position(
        self,
        symbol: str,
        direction: str,
        size: float,
        entry_price: float
    ) -> dict:
        """
        Open a new position.
        
        Args:
            symbol: Trading symbol
            direction: 'long' or 'short'
            size: Position size (units or lots)
            entry_price: Entry price
            
        Returns:
            Position details or error
        """
        position_value = size * entry_price
        margin_required = position_value * (self.initial_margin_pct / 100)
        
        # Check if enough margin
        if margin_required > self.free_margin:
            return {'error': 'Insufficient margin'}
        
        position = {
            'id': len(self.positions) + 1,
            'symbol': symbol,
            'direction': direction,
            'size': size,
            'entry_price': entry_price,
            'current_price': entry_price,
            'current_value': position_value,
            'margin_used': margin_required,
            'unrealized_pnl': 0
        }
        
        self.positions.append(position)
        return position
    
    def update_prices(self, prices: Dict[str, float]):
        """
        Update positions with new prices.
        
        Args:
            prices: Dict of symbol -> current price
        """
        for pos in self.positions:
            if pos['symbol'] in prices:
                new_price = prices[pos['symbol']]
                pos['current_price'] = new_price
                pos['current_value'] = pos['size'] * new_price
                
                # Calculate P&L
                price_change = new_price - pos['entry_price']
                if pos['direction'] == 'short':
                    price_change = -price_change
                pos['unrealized_pnl'] = price_change * pos['size']
    
    def check_margin_call(self) -> dict:
        """
        Check if account is in margin call.
        
        Returns:
            Margin call status
        """
        maintenance_required = self.used_margin * (self.maintenance_margin_pct / self.initial_margin_pct)
        
        if self.equity <= 0:
            return {
                'status': 'LIQUIDATION',
                'message': 'Account equity depleted',
                'equity': self.equity
            }
        elif self.equity < maintenance_required:
            return {
                'status': 'MARGIN_CALL',
                'message': 'Below maintenance margin',
                'equity': self.equity,
                'required': maintenance_required,
                'shortfall': maintenance_required - self.equity
            }
        else:
            return {
                'status': 'OK',
                'margin_level': self.margin_level
            }
    
    def summary(self) -> dict:
        """Get account summary."""
        return {
            'initial_balance': self.initial_balance,
            'equity': round(self.equity, 2),
            'unrealized_pnl': round(self.unrealized_pnl, 2),
            'used_margin': round(self.used_margin, 2),
            'free_margin': round(self.free_margin, 2),
            'margin_level': round(self.margin_level, 2) if self.margin_level != float('inf') else 'N/A',
            'leverage': f"{self.leverage_ratio:.0f}:1",
            'positions': len(self.positions)
        }


# Demonstrate margin account
account = MarginAccount(balance=10000, initial_margin_pct=2.0, maintenance_margin_pct=1.0)

print("Initial Account State")
print("=" * 40)
for k, v in account.summary().items():
    print(f"{k}: {v}")
# Open a position and track margin
position = account.open_position('EUR/USD', 'long', 100000, 1.0850)

print("\nAfter Opening Position")
print("=" * 40)
print(f"Position: 100,000 EUR/USD at 1.0850")
print(f"Position Value: ${100000 * 1.0850:,.2f}")
print(f"Margin Used: ${position['margin_used']:,.2f}")
print("\nAccount:")
for k, v in account.summary().items():
    print(f"  {k}: {v}")
# Simulate price movement and margin call
price_scenarios = [1.0850, 1.0800, 1.0750, 1.0700, 1.0650, 1.0600]

print("\nPrice Movement Scenarios")
print("=" * 70)
print(f"{'Price':<10} {'P&L':<12} {'Equity':<12} {'Margin Level':<15} {'Status'}")
print("-" * 70)

for price in price_scenarios:
    account.update_prices({'EUR/USD': price})
    status = account.check_margin_call()
    summary = account.summary()
    
    pnl = summary['unrealized_pnl']
    equity = summary['equity']
    margin_lvl = summary['margin_level']
    
    pnl_str = f"${pnl:+,.0f}"
    equity_str = f"${equity:,.0f}"
    margin_str = f"{margin_lvl}%" if margin_lvl != 'N/A' else 'N/A'
    
    print(f"{price:<10} {pnl_str:<12} {equity_str:<12} {margin_str:<15} {status['status']}")

Exercise 3.1: Margin Calculator (Guided)

Complete the function to calculate margin requirements for different instruments.

Exercise
Solution 3.1
def calculate_margin_requirements(
    instrument_type: str,
    position_value: float,
    leverage: float = None
) -> dict:
    """
    Calculate margin requirements for different instruments.
    """
    # Default leverage by instrument type
    default_leverage = {
        'forex': 50,
        'futures': 15,
        'stocks': 2
    }

    # Get leverage (use default if not specified)
    if leverage is None:
        leverage = default_leverage.get(instrument_type.lower(), 1)  # Get with default

    # Calculate margin percentage
    margin_pct = 100 / leverage  # 100 divided by leverage

    # Calculate initial margin
    initial_margin = position_value * (margin_pct / 100)

    # Maintenance margin (typically 50% of initial for forex)
    maintenance_margin = initial_margin * 0.5  # 50% = 0.5

    # Calculate max position from margin
    max_position_from_margin = initial_margin * leverage

    return {
        'instrument': instrument_type,
        'position_value': position_value,
        'leverage': f"{leverage}:1",
        'margin_pct': f"{margin_pct:.2f}%",
        'initial_margin': round(initial_margin, 2),
        'maintenance_margin': round(maintenance_margin, 2),
        'max_adverse_move': f"{100/leverage:.2f}%"
    }

3.3 Position Sizing with Leverage

The Risk Equation

Risk Amount = Account Balance × Risk Percentage
Position Size = Risk Amount / (Stop Loss in Pips × Pip Value)

Professional Position Sizing Rules

  1. Risk per trade: 1-2% of account
  2. Never risk more than you can afford to lose
  3. Account for correlation when holding multiple positions
  4. Leave margin buffer for adverse moves
class LeveragedPositionSizer:
    """
    Calculate position sizes accounting for leverage.
    
    Attributes:
        account_balance: Trading account balance
        leverage: Available leverage ratio
    """
    
    def __init__(self, account_balance: float, leverage: float = 50):
        self.account_balance = account_balance
        self.leverage = leverage
    
    @property
    def max_position_value(self) -> float:
        """Maximum position value based on leverage."""
        return self.account_balance * self.leverage
    
    def size_by_risk(
        self,
        risk_pct: float,
        stop_loss_pips: int,
        pip_value: float = 10.0
    ) -> dict:
        """
        Calculate position size based on risk.
        
        Args:
            risk_pct: Percentage of account to risk
            stop_loss_pips: Stop loss distance in pips
            pip_value: Dollar value per pip per lot
            
        Returns:
            Position sizing details
        """
        # Calculate risk amount in dollars
        risk_amount = self.account_balance * (risk_pct / 100)
        
        # Calculate position size in lots
        # Risk per lot = stop_loss_pips * pip_value
        risk_per_lot = stop_loss_pips * pip_value
        lots = risk_amount / risk_per_lot
        
        # Calculate position value
        position_value = lots * 100_000  # Standard lot = 100,000 units
        
        # Check if within leverage limits
        margin_required = position_value / self.leverage
        within_limits = position_value <= self.max_position_value
        
        return {
            'risk_pct': risk_pct,
            'risk_amount': round(risk_amount, 2),
            'stop_loss_pips': stop_loss_pips,
            'lots': round(lots, 2),
            'units': int(lots * 100_000),
            'position_value': round(position_value, 2),
            'margin_required': round(margin_required, 2),
            'margin_pct_of_account': round((margin_required / self.account_balance) * 100, 1),
            'within_limits': within_limits
        }
    
    def size_by_margin(
        self,
        margin_pct: float,
        current_price: float = 1.0
    ) -> dict:
        """
        Calculate position size based on margin usage.
        
        Args:
            margin_pct: Percentage of account to use as margin
            current_price: Current price for the instrument
            
        Returns:
            Position sizing details
        """
        margin_used = self.account_balance * (margin_pct / 100)
        position_value = margin_used * self.leverage
        units = position_value / current_price
        lots = units / 100_000
        
        return {
            'margin_pct': margin_pct,
            'margin_used': round(margin_used, 2),
            'position_value': round(position_value, 2),
            'units': int(units),
            'lots': round(lots, 2),
            'remaining_margin': round(self.account_balance - margin_used, 2)
        }


# Example position sizing
sizer = LeveragedPositionSizer(account_balance=10000, leverage=50)

print("Position Sizing Examples")
print("=" * 60)
print(f"Account: ${sizer.account_balance:,} | Leverage: {sizer.leverage}:1")
print(f"Max Position Value: ${sizer.max_position_value:,}")

# Size by risk
print("\n--- Size by Risk ---")
for risk in [1, 2, 3]:
    result = sizer.size_by_risk(risk_pct=risk, stop_loss_pips=50)
    print(f"\n{risk}% Risk, 50 pip stop:")
    print(f"  Risk Amount: ${result['risk_amount']}")
    print(f"  Position: {result['lots']} lots ({result['units']:,} units)")
    print(f"  Margin Required: ${result['margin_required']} ({result['margin_pct_of_account']}% of account)")

Exercise 3.2: Leveraged Position Sizing (Guided)

Complete the function to calculate optimal position size considering both risk and margin.

Exercise
Solution 3.2
def calculate_safe_position(
    account_balance: float,
    leverage: float,
    risk_pct: float,
    stop_loss_pips: int,
    max_margin_pct: float = 50.0,
    pip_value: float = 10.0
) -> dict:
    """
    Calculate safe position size considering both risk and margin limits.
    """
    # Calculate position by risk
    risk_amount = account_balance * (risk_pct / 100)
    risk_per_lot = stop_loss_pips * pip_value
    lots_by_risk = risk_amount / risk_per_lot  # Divide by risk per lot

    # Calculate position by margin limit
    max_margin = account_balance * (max_margin_pct / 100)
    max_position_value = max_margin * leverage  # Multiply by leverage
    lots_by_margin = max_position_value / 100_000  # Convert to lots

    # Use the smaller of the two
    final_lots = min(lots_by_risk, lots_by_margin)  # Take minimum
    limiting_factor = 'risk' if lots_by_risk < lots_by_margin else 'margin'

    # Calculate final metrics
    position_value = final_lots * 100_000
    actual_margin = position_value / leverage
    actual_risk = final_lots * risk_per_lot

    return {
        'lots_by_risk': round(lots_by_risk, 3),
        'lots_by_margin': round(lots_by_margin, 3),
        'final_lots': round(final_lots, 3),
        'limiting_factor': limiting_factor,
        'position_value': round(position_value, 2),
        'margin_used': round(actual_margin, 2),
        'margin_pct': round((actual_margin / account_balance) * 100, 1),
        'actual_risk': round(actual_risk, 2),
        'actual_risk_pct': round((actual_risk / account_balance) * 100, 2)
    }

3.4 Leverage Risk Management

Why Most Traders Lose with Leverage

  1. Over-leveraging: Using too much of available leverage
  2. No stop losses: Letting losses run
  3. Ignoring volatility: Same leverage in all market conditions
  4. Correlation blindness: Multiple correlated positions multiply risk

Professional Approaches

Approach Description Typical Leverage Used
Conservative Long-term, low stress 2:1 - 5:1
Moderate Balanced risk/reward 5:1 - 15:1
Aggressive Active trading 15:1 - 30:1
Extreme (Not Recommended) Day trading 30:1+
class RiskScenarioAnalyzer:
    """
    Analyze risk scenarios for leveraged positions.
    
    Attributes:
        account_balance: Starting balance
        leverage: Leverage ratio
    """
    
    def __init__(self, account_balance: float, leverage: float):
        self.account_balance = account_balance
        self.leverage = leverage
    
    def simulate_price_path(
        self,
        position_pct: float,
        daily_volatility: float,
        days: int,
        simulations: int = 1000
    ) -> pd.DataFrame:
        """
        Simulate equity paths with leverage.
        
        Args:
            position_pct: Percentage of max leverage used
            daily_volatility: Daily price volatility (e.g., 0.01 = 1%)
            days: Number of days to simulate
            simulations: Number of simulation paths
            
        Returns:
            DataFrame with simulation results
        """
        # Calculate effective leverage used
        effective_leverage = self.leverage * (position_pct / 100)
        
        # Generate random returns
        np.random.seed(42)
        returns = np.random.normal(0, daily_volatility, (simulations, days))
        
        # Apply leverage to returns
        leveraged_returns = returns * effective_leverage
        
        # Calculate equity paths
        equity_paths = self.account_balance * np.cumprod(1 + leveraged_returns, axis=1)
        
        # Identify blown accounts (equity <= 0)
        blown = np.any(equity_paths <= 0, axis=1)
        
        return {
            'final_equities': equity_paths[:, -1],
            'max_drawdowns': self._calculate_drawdowns(equity_paths),
            'blown_accounts': blown.sum(),
            'blow_up_rate': blown.mean() * 100,
            'mean_final': equity_paths[:, -1].mean(),
            'median_final': np.median(equity_paths[:, -1]),
            'percentile_5': np.percentile(equity_paths[:, -1], 5),
            'percentile_95': np.percentile(equity_paths[:, -1], 95)
        }
    
    def _calculate_drawdowns(self, equity_paths: np.ndarray) -> np.ndarray:
        """Calculate maximum drawdown for each path."""
        running_max = np.maximum.accumulate(equity_paths, axis=1)
        drawdowns = (running_max - equity_paths) / running_max
        return np.max(drawdowns, axis=1)
    
    def compare_leverage_levels(
        self,
        position_pcts: List[float],
        daily_volatility: float = 0.01,
        days: int = 252
    ) -> pd.DataFrame:
        """
        Compare outcomes at different leverage levels.
        
        Args:
            position_pcts: List of position percentages to test
            daily_volatility: Daily volatility
            days: Simulation period
            
        Returns:
            Comparison DataFrame
        """
        results = []
        for pct in position_pcts:
            sim = self.simulate_price_path(pct, daily_volatility, days)
            effective_lev = self.leverage * (pct / 100)
            results.append({
                'Position %': f"{pct}%",
                'Effective Leverage': f"{effective_lev:.1f}:1",
                'Blow-up Rate': f"{sim['blow_up_rate']:.1f}%",
                'Mean Final': f"${sim['mean_final']:,.0f}",
                'Median Final': f"${sim['median_final']:,.0f}",
                '5th Percentile': f"${sim['percentile_5']:,.0f}",
                'Max DD (avg)': f"{np.mean(sim['max_drawdowns'])*100:.0f}%"
            })
        return pd.DataFrame(results)


# Analyze different leverage scenarios
analyzer = RiskScenarioAnalyzer(account_balance=10000, leverage=50)

print("Leverage Risk Comparison (1 Year, 1% Daily Volatility)")
print("=" * 90)
comparison = analyzer.compare_leverage_levels([10, 25, 50, 75, 100])
print(comparison.to_string(index=False))
# Visualize risk scenarios
def plot_leverage_risk():
    """Visualize the relationship between leverage and risk."""
    analyzer = RiskScenarioAnalyzer(10000, 50)
    
    position_pcts = [5, 10, 20, 30, 50, 75, 100]
    blow_up_rates = []
    median_returns = []
    
    for pct in position_pcts:
        sim = analyzer.simulate_price_path(pct, 0.01, 252, 500)
        blow_up_rates.append(sim['blow_up_rate'])
        median_returns.append((sim['median_final'] / 10000 - 1) * 100)
    
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    
    # Blow-up rate vs leverage
    ax1 = axes[0]
    effective_leverage = [50 * p / 100 for p in position_pcts]
    ax1.bar(range(len(position_pcts)), blow_up_rates, color='red', alpha=0.7)
    ax1.set_xticks(range(len(position_pcts)))
    ax1.set_xticklabels([f"{l:.0f}:1" for l in effective_leverage])
    ax1.set_xlabel('Effective Leverage')
    ax1.set_ylabel('Account Blow-up Rate (%)')
    ax1.set_title('Probability of Losing Everything (1 Year)')
    ax1.grid(True, alpha=0.3)
    
    # Add danger zone annotation
    ax1.axhline(y=10, color='orange', linestyle='--', label='10% threshold')
    ax1.legend()
    
    # Risk-return tradeoff
    ax2 = axes[1]
    colors = ['green' if r > 0 else 'red' for r in median_returns]
    ax2.bar(range(len(position_pcts)), median_returns, color=colors, alpha=0.7)
    ax2.set_xticks(range(len(position_pcts)))
    ax2.set_xticklabels([f"{l:.0f}:1" for l in effective_leverage])
    ax2.set_xlabel('Effective Leverage')
    ax2.set_ylabel('Median Annual Return (%)')
    ax2.set_title('Median Return vs Leverage')
    ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()

plot_leverage_risk()

Exercise 3.3: Risk Scenarios (Guided)

Complete the function to analyze drawdown scenarios with leverage.

Exercise
Solution 3.3
def analyze_drawdown_recovery(
    initial_balance: float,
    leverage: float,
    drawdown_pct: float,
    daily_return: float = 0.001
) -> dict:
    """
    Analyze drawdown impact and recovery time with leverage.
    """
    # Calculate balance after drawdown
    balance_after = initial_balance * (1 - drawdown_pct / 100)  # Convert pct to decimal

    # Calculate loss amount
    loss_amount = initial_balance - balance_after

    # Calculate required return to recover
    required_return = (initial_balance / balance_after - 1) * 100  # Convert to percentage

    # Calculate price move that caused this (accounting for leverage)
    price_move = drawdown_pct / leverage  # Divide by leverage

    # Estimate recovery days (simplified)
    leveraged_daily_return = daily_return * leverage
    if leveraged_daily_return > 0:
        recovery_days = np.log(initial_balance / balance_after) / np.log(1 + leveraged_daily_return)
    else:
        recovery_days = float('inf')

    return {
        'initial_balance': initial_balance,
        'drawdown_pct': f"{drawdown_pct}%",
        'balance_after': round(balance_after, 2),
        'loss_amount': round(loss_amount, 2),
        'price_move_caused': f"{price_move:.2f}%",
        'required_return_to_recover': f"{required_return:.1f}%",
        'estimated_recovery_days': int(recovery_days) if recovery_days != float('inf') else 'N/A'
    }

Exercise 3.4: Margin Account Simulator (Open-ended)

Build a margin account simulator that tracks positions, margin, and handles margin calls.

Your implementation:

Exercise
Solution 3.4
class MarginAccountSimulator:
    """
    Full margin account simulator with liquidation.
    """

    def __init__(
        self,
        balance: float,
        leverage: float = 50,
        maintenance_pct: float = 50
    ):
        self.initial_balance = balance
        self.cash = balance
        self.leverage = leverage
        self.initial_margin_pct = 100 / leverage
        self.maintenance_pct = maintenance_pct

        self.positions = {}
        self.position_counter = 0
        self.trade_history = []

    @property
    def unrealized_pnl(self) -> float:
        return sum(p['unrealized_pnl'] for p in self.positions.values())

    @property
    def equity(self) -> float:
        return self.cash + self.unrealized_pnl

    @property
    def used_margin(self) -> float:
        return sum(p['margin'] for p in self.positions.values())

    @property
    def margin_level(self) -> float:
        if self.used_margin == 0:
            return float('inf')
        return (self.equity / self.used_margin) * 100

    def open_position(self, symbol: str, direction: str, size: float, price: float) -> dict:
        position_value = size * price
        margin_required = position_value * (self.initial_margin_pct / 100)

        if margin_required > (self.equity - self.used_margin):
            return {'error': 'Insufficient margin'}

        self.position_counter += 1
        position = {
            'id': self.position_counter,
            'symbol': symbol,
            'direction': direction,
            'size': size,
            'entry_price': price,
            'current_price': price,
            'margin': margin_required,
            'unrealized_pnl': 0
        }
        self.positions[self.position_counter] = position
        return position

    def close_position(self, position_id: int, price: float) -> dict:
        if position_id not in self.positions:
            return {'error': 'Position not found'}

        pos = self.positions[position_id]
        pos['current_price'] = price

        pnl = (price - pos['entry_price']) * pos['size']
        if pos['direction'] == 'short':
            pnl = -pnl

        self.cash += pnl
        del self.positions[position_id]

        self.trade_history.append({
            'position_id': position_id,
            'symbol': pos['symbol'],
            'realized_pnl': pnl
        })

        return {'realized_pnl': pnl}

    def update_prices(self, prices: Dict[str, float]):
        for pos in self.positions.values():
            if pos['symbol'] in prices:
                pos['current_price'] = prices[pos['symbol']]
                pnl = (pos['current_price'] - pos['entry_price']) * pos['size']
                if pos['direction'] == 'short':
                    pnl = -pnl
                pos['unrealized_pnl'] = pnl

    def check_margin_status(self) -> dict:
        margin_call_level = 100  # 100% margin level = margin call
        liquidation_level = 50   # 50% margin level = liquidation

        if self.margin_level <= liquidation_level:
            return {'status': 'LIQUIDATION', 'margin_level': self.margin_level}
        elif self.margin_level <= margin_call_level:
            return {'status': 'MARGIN_CALL', 'margin_level': self.margin_level}
        else:
            return {'status': 'OK', 'margin_level': self.margin_level}

    def liquidate_if_needed(self) -> List[dict]:
        status = self.check_margin_status()
        liquidated = []

        if status['status'] == 'LIQUIDATION':
            # Close all positions
            for pos_id in list(self.positions.keys()):
                pos = self.positions[pos_id]
                result = self.close_position(pos_id, pos['current_price'])
                liquidated.append({
                    'position_id': pos_id,
                    'symbol': pos['symbol'],
                    'pnl': result.get('realized_pnl', 0)
                })

        return liquidated


# Test the simulator
sim = MarginAccountSimulator(10000, leverage=50)
sim.open_position('EUR/USD', 'long', 100000, 1.0850)

# Simulate price drop
for price in [1.0850, 1.0800, 1.0750, 1.0700]:
    sim.update_prices({'EUR/USD': price})
    status = sim.check_margin_status()
    print(f"Price: {price} | Equity: ${sim.equity:,.0f} | "
          f"Margin Level: {sim.margin_level:.0f}% | Status: {status['status']}")

    liq = sim.liquidate_if_needed()
    if liq:
        print(f"  LIQUIDATED: {liq}")
        break

Exercise 3.5: Portfolio Leverage Manager (Open-ended)

Build a portfolio leverage manager that tracks total exposure across multiple positions.

Your implementation:

Exercise
Solution 3.5
class PortfolioLeverageManager:
    """
    Manage leverage across a portfolio of positions.
    """

    def __init__(
        self,
        account_balance: float,
        max_total_leverage: float = 10,
        max_correlated_leverage: float = 5
    ):
        self.account_balance = account_balance
        self.max_total_leverage = max_total_leverage
        self.max_correlated_leverage = max_correlated_leverage
        self.positions = []

    def add_position(self, symbol: str, value: float, correlation_group: str):
        self.positions.append({
            'symbol': symbol,
            'value': value,
            'correlation_group': correlation_group
        })

    def get_total_leverage(self) -> float:
        total_value = sum(p['value'] for p in self.positions)
        return total_value / self.account_balance

    def get_correlated_exposure(self, group: str) -> float:
        group_value = sum(
            p['value'] for p in self.positions
            if p['correlation_group'] == group
        )
        return group_value / self.account_balance

    def recommend_new_position(self, symbol: str, desired_value: float, group: str) -> dict:
        current_leverage = self.get_total_leverage()
        new_leverage = current_leverage + (desired_value / self.account_balance)

        current_group = self.get_correlated_exposure(group)
        new_group = current_group + (desired_value / self.account_balance)

        # Check limits
        total_ok = new_leverage <= self.max_total_leverage
        group_ok = new_group <= self.max_correlated_leverage

        if total_ok and group_ok:
            recommendation = 'APPROVED'
            suggested_value = desired_value
        else:
            recommendation = 'REDUCE'
            # Calculate max allowed
            max_by_total = (self.max_total_leverage - current_leverage) * self.account_balance
            max_by_group = (self.max_correlated_leverage - current_group) * self.account_balance
            suggested_value = max(0, min(max_by_total, max_by_group))

        return {
            'symbol': symbol,
            'requested': desired_value,
            'recommendation': recommendation,
            'suggested_value': round(suggested_value, 2),
            'new_total_leverage': round(new_leverage, 2),
            'new_group_leverage': round(new_group, 2)
        }

    def risk_report(self) -> pd.DataFrame:
        groups = set(p['correlation_group'] for p in self.positions)

        results = []
        for group in groups:
            group_positions = [p for p in self.positions if p['correlation_group'] == group]
            group_value = sum(p['value'] for p in group_positions)

            results.append({
                'Group': group,
                'Positions': len(group_positions),
                'Total Value': f"${group_value:,.0f}",
                'Leverage': f"{group_value/self.account_balance:.1f}x",
                'Status': 'OK' if group_value/self.account_balance <= self.max_correlated_leverage else 'WARNING'
            })

        return pd.DataFrame(results)


# Test
manager = PortfolioLeverageManager(10000, max_total_leverage=10, max_correlated_leverage=5)

manager.add_position('EUR/USD', 30000, 'USD')
manager.add_position('GBP/USD', 20000, 'USD')
manager.add_position('USD/JPY', 15000, 'JPY')

print("Portfolio Risk Report")
print(manager.risk_report().to_string(index=False))
print(f"\nTotal Leverage: {manager.get_total_leverage():.1f}x")

# Check new position
rec = manager.recommend_new_position('AUD/USD', 30000, 'USD')
print(f"\nNew Position Recommendation: {rec}")

Exercise 3.6: Leverage Risk Calculator (Open-ended)

Build a comprehensive leverage risk calculator for pre-trade analysis.

Your implementation:

Exercise
Solution 3.6
class LeverageRiskCalculator:
    """
    Pre-trade leverage risk analysis.
    """

    def __init__(self):
        self.account = None
        self.leverage = None
        self.volatility = None

    def set_parameters(self, account: float, leverage: float, volatility: float):
        self.account = account
        self.leverage = leverage
        self.volatility = volatility

    def calculate_max_position(self) -> dict:
        max_position = self.account * self.leverage
        margin_required = self.account  # Using 100% of account as margin
        wipeout_move = 100 / self.leverage

        # Safe position (account for 3-sigma move)
        three_sigma = self.volatility * 3
        safe_leverage = min(self.leverage, 100 / (three_sigma * 100) * 0.5)
        safe_position = self.account * safe_leverage

        return {
            'max_position': round(max_position, 2),
            'margin_required': round(margin_required, 2),
            'wipeout_move_pct': round(wipeout_move, 2),
            'recommended_leverage': round(safe_leverage, 1),
            'safe_position': round(safe_position, 2),
            'daily_var_95': round(self.volatility * 1.65 * safe_leverage * self.account, 2)
        }

    def margin_call_price(self, entry_price: float, direction: str) -> float:
        margin_pct = 100 / self.leverage
        maintenance_pct = margin_pct * 0.5  # 50% maintenance

        # Price move to hit maintenance
        move_to_maintenance = (margin_pct - maintenance_pct) / 100

        if direction == 'long':
            return entry_price * (1 - move_to_maintenance)
        else:
            return entry_price * (1 + move_to_maintenance)

    def scenario_analysis(self, position_value: float, scenarios: List[float]) -> pd.DataFrame:
        results = []
        margin = position_value / self.leverage

        for change_pct in scenarios:
            pnl = position_value * (change_pct / 100)
            new_equity = self.account + pnl
            margin_level = (new_equity / margin) * 100 if margin > 0 else 0

            results.append({
                'Price Change': f"{change_pct:+.1f}%",
                'P&L': f"${pnl:+,.0f}",
                'Equity': f"${new_equity:,.0f}",
                'Margin Level': f"{margin_level:.0f}%",
                'Status': 'OK' if margin_level > 100 else 'MARGIN CALL' if margin_level > 50 else 'LIQUIDATION'
            })

        return pd.DataFrame(results)

    def plot_risk_profile(self):
        if self.account is None:
            print("Set parameters first")
            return

        fig, axes = plt.subplots(1, 2, figsize=(14, 5))

        # Left: Equity vs price change
        ax1 = axes[0]
        price_changes = np.linspace(-5, 5, 100)

        for lev in [5, 10, 25, 50]:
            position = self.account * lev
            equity = self.account + position * (price_changes / 100)
            ax1.plot(price_changes, equity / 1000, label=f'{lev}:1')

        ax1.axhline(y=0, color='red', linestyle='--', alpha=0.7)
        ax1.set_xlabel('Price Change (%)')
        ax1.set_ylabel('Equity ($1000s)')
        ax1.set_title('Equity vs Price Change at Different Leverage')
        ax1.legend()
        ax1.grid(True, alpha=0.3)

        # Right: VaR by leverage
        ax2 = axes[1]
        leverage_range = range(1, 51)
        var_95 = [self.volatility * 1.65 * l * self.account for l in leverage_range]

        ax2.fill_between(leverage_range, var_95, alpha=0.3, color='red')
        ax2.plot(leverage_range, var_95, color='red', linewidth=2)
        ax2.axhline(y=self.account, color='black', linestyle='--', label='Account Balance')

        ax2.set_xlabel('Leverage')
        ax2.set_ylabel('Daily VaR (95%)')
        ax2.set_title('Value at Risk vs Leverage')
        ax2.legend()
        ax2.grid(True, alpha=0.3)

        plt.tight_layout()
        plt.show()


# Test
calc = LeverageRiskCalculator()
calc.set_parameters(account=10000, leverage=50, volatility=0.01)

print("Max Position Analysis")
for k, v in calc.calculate_max_position().items():
    print(f"  {k}: {v}")

print(f"\nMargin Call Price (Long from 1.0850): {calc.margin_call_price(1.0850, 'long'):.4f}")

print("\nScenario Analysis (Position: $200,000)")
print(calc.scenario_analysis(200000, [-3, -2, -1, 0, 1, 2, 3]).to_string(index=False))

calc.plot_risk_profile()

Module Project: Leverage Risk Calculator

Build a comprehensive leverage risk management system.

class LeverageRiskManager:
    """
    Comprehensive leverage and risk management system.
    
    Combines margin calculation, position sizing, scenario analysis,
    and risk visualization for leveraged trading.
    """
    
    def __init__(
        self,
        account_balance: float,
        max_leverage: float = 50,
        risk_per_trade: float = 2.0,
        max_portfolio_risk: float = 10.0
    ):
        self.account_balance = account_balance
        self.max_leverage = max_leverage
        self.risk_per_trade = risk_per_trade
        self.max_portfolio_risk = max_portfolio_risk
        
        self.positions = []
    
    # ==================== Position Sizing ====================
    
    def calculate_position_size(
        self,
        stop_loss_pips: int,
        pip_value: float = 10.0,
        custom_risk_pct: float = None
    ) -> dict:
        """
        Calculate optimal position size.
        
        Args:
            stop_loss_pips: Stop loss distance in pips
            pip_value: Value per pip per lot
            custom_risk_pct: Override default risk percentage
            
        Returns:
            Position sizing details
        """
        risk_pct = custom_risk_pct or self.risk_per_trade
        
        # Calculate by risk
        risk_amount = self.account_balance * (risk_pct / 100)
        risk_per_lot = stop_loss_pips * pip_value
        lots_by_risk = risk_amount / risk_per_lot
        
        # Calculate by leverage limit
        max_position_value = self.account_balance * self.max_leverage
        lots_by_leverage = max_position_value / 100_000
        
        # Use conservative sizing
        recommended_lots = min(lots_by_risk, lots_by_leverage)
        position_value = recommended_lots * 100_000
        margin_required = position_value / self.max_leverage
        effective_leverage = position_value / self.account_balance
        
        return {
            'risk_pct': risk_pct,
            'risk_amount': round(risk_amount, 2),
            'stop_loss_pips': stop_loss_pips,
            'lots': round(recommended_lots, 3),
            'units': int(recommended_lots * 100_000),
            'position_value': round(position_value, 2),
            'margin_required': round(margin_required, 2),
            'effective_leverage': round(effective_leverage, 1),
            'margin_pct_of_account': round((margin_required / self.account_balance) * 100, 1)
        }
    
    # ==================== Margin Analysis ====================
    
    def calculate_margin_call_level(
        self,
        entry_price: float,
        position_value: float,
        direction: str = 'long',
        maintenance_pct: float = 50
    ) -> dict:
        """
        Calculate margin call price level.
        
        Args:
            entry_price: Position entry price
            position_value: Total position value
            direction: 'long' or 'short'
            maintenance_pct: Maintenance margin percentage
            
        Returns:
            Margin call analysis
        """
        margin_used = position_value / self.max_leverage
        maintenance_margin = margin_used * (maintenance_pct / 100)
        
        # Calculate price move to margin call
        equity_buffer = self.account_balance - maintenance_margin
        price_move_pct = (equity_buffer / position_value) * 100
        
        if direction == 'long':
            margin_call_price = entry_price * (1 - price_move_pct / 100)
        else:
            margin_call_price = entry_price * (1 + price_move_pct / 100)
        
        # Calculate liquidation price (assume 20% maintenance)
        liquidation_buffer = self.account_balance - margin_used * 0.2
        liq_move_pct = (liquidation_buffer / position_value) * 100
        
        if direction == 'long':
            liquidation_price = entry_price * (1 - liq_move_pct / 100)
        else:
            liquidation_price = entry_price * (1 + liq_move_pct / 100)
        
        return {
            'entry_price': entry_price,
            'direction': direction,
            'position_value': position_value,
            'margin_used': round(margin_used, 2),
            'margin_call_price': round(margin_call_price, 5),
            'margin_call_pips': abs(round((entry_price - margin_call_price) / 0.0001)),
            'liquidation_price': round(liquidation_price, 5),
            'buffer_pct': round(price_move_pct, 2)
        }
    
    # ==================== Scenario Analysis ====================
    
    def scenario_analysis(
        self,
        position_value: float,
        price_scenarios: List[float] = None
    ) -> pd.DataFrame:
        """
        Analyze P&L under different price scenarios.
        
        Args:
            position_value: Total position value
            price_scenarios: List of price change percentages
            
        Returns:
            Scenario analysis DataFrame
        """
        if price_scenarios is None:
            price_scenarios = [-5, -3, -2, -1, -0.5, 0, 0.5, 1, 2, 3, 5]
        
        margin_used = position_value / self.max_leverage
        
        results = []
        for change_pct in price_scenarios:
            pnl = position_value * (change_pct / 100)
            new_equity = self.account_balance + pnl
            margin_level = (new_equity / margin_used) * 100 if margin_used > 0 else float('inf')
            account_change = (pnl / self.account_balance) * 100
            
            if margin_level <= 20:
                status = 'LIQUIDATION'
            elif margin_level <= 50:
                status = 'MARGIN CALL'
            elif margin_level <= 100:
                status = 'WARNING'
            else:
                status = 'OK'
            
            results.append({
                'Price Change': f"{change_pct:+.1f}%",
                'P&L': f"${pnl:+,.0f}",
                'Account Change': f"{account_change:+.1f}%",
                'Equity': f"${new_equity:,.0f}",
                'Margin Level': f"{margin_level:.0f}%",
                'Status': status
            })
        
        return pd.DataFrame(results)
    
    # ==================== Risk Metrics ====================
    
    def calculate_risk_metrics(
        self,
        position_value: float,
        daily_volatility: float = 0.01
    ) -> dict:
        """
        Calculate risk metrics for a position.
        
        Args:
            position_value: Total position value
            daily_volatility: Expected daily volatility
            
        Returns:
            Risk metrics dictionary
        """
        effective_leverage = position_value / self.account_balance
        
        # Daily VaR (95%)
        var_95 = daily_volatility * 1.65 * position_value
        
        # Daily VaR (99%)
        var_99 = daily_volatility * 2.33 * position_value
        
        # Expected daily P&L range
        daily_range = daily_volatility * position_value
        
        # Days to potential wipeout (99% VaR)
        if var_99 > 0:
            days_to_wipeout = self.account_balance / var_99
        else:
            days_to_wipeout = float('inf')
        
        # Price move to lose entire account
        wipeout_move = (self.account_balance / position_value) * 100
        
        return {
            'position_value': position_value,
            'effective_leverage': round(effective_leverage, 1),
            'daily_volatility': f"{daily_volatility*100:.1f}%",
            'daily_var_95': round(var_95, 2),
            'daily_var_95_pct': round((var_95 / self.account_balance) * 100, 1),
            'daily_var_99': round(var_99, 2),
            'expected_daily_range': round(daily_range, 2),
            'wipeout_price_move': round(wipeout_move, 2),
            'theoretical_days_to_wipeout': round(days_to_wipeout, 1)
        }
    
    # ==================== Visualization ====================
    
    def plot_risk_profile(self, position_value: float = None):
        """Visualize risk profile."""
        if position_value is None:
            position_value = self.account_balance * 10  # Default 10:1
        
        fig, axes = plt.subplots(2, 2, figsize=(14, 10))
        
        # 1. Equity curve by price change
        ax1 = axes[0, 0]
        price_changes = np.linspace(-5, 5, 100)
        equity = self.account_balance + position_value * (price_changes / 100)
        
        ax1.fill_between(price_changes, equity, self.account_balance, 
                        where=equity > self.account_balance, alpha=0.3, color='green')
        ax1.fill_between(price_changes, equity, self.account_balance,
                        where=equity < self.account_balance, alpha=0.3, color='red')
        ax1.plot(price_changes, equity, 'b-', linewidth=2)
        ax1.axhline(y=0, color='red', linestyle='--', label='Wipeout')
        ax1.axhline(y=self.account_balance, color='black', linestyle='-', alpha=0.3)
        ax1.set_xlabel('Price Change (%)')
        ax1.set_ylabel('Account Equity ($)')
        ax1.set_title(f'Equity vs Price Change (Position: ${position_value:,.0f})')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # 2. Leverage comparison
        ax2 = axes[0, 1]
        leverage_levels = [5, 10, 25, 50]
        colors = ['green', 'blue', 'orange', 'red']
        
        for lev, color in zip(leverage_levels, colors):
            pos_val = self.account_balance * lev
            eq = self.account_balance + pos_val * (price_changes / 100)
            ax2.plot(price_changes, eq / 1000, label=f'{lev}:1', color=color, linewidth=2)
        
        ax2.axhline(y=0, color='red', linestyle='--', alpha=0.7)
        ax2.set_xlabel('Price Change (%)')
        ax2.set_ylabel('Equity ($1000s)')
        ax2.set_title('Equity by Leverage Level')
        ax2.legend()
        ax2.grid(True, alpha=0.3)
        
        # 3. VaR by leverage
        ax3 = axes[1, 0]
        leverage_range = range(1, 51)
        daily_vol = 0.01
        var_95 = [daily_vol * 1.65 * self.account_balance * l for l in leverage_range]
        var_99 = [daily_vol * 2.33 * self.account_balance * l for l in leverage_range]
        
        ax3.fill_between(leverage_range, var_99, alpha=0.3, color='red', label='99% VaR')
        ax3.fill_between(leverage_range, var_95, alpha=0.3, color='orange', label='95% VaR')
        ax3.axhline(y=self.account_balance, color='black', linestyle='--', 
                   label='Account Balance')
        ax3.set_xlabel('Leverage')
        ax3.set_ylabel('Daily VaR ($)')
        ax3.set_title('Value at Risk vs Leverage (1% daily vol)')
        ax3.legend()
        ax3.grid(True, alpha=0.3)
        
        # 4. Margin level by price change
        ax4 = axes[1, 1]
        margin_used = position_value / self.max_leverage
        margin_levels = ((self.account_balance + position_value * (price_changes / 100)) 
                        / margin_used) * 100
        
        ax4.plot(price_changes, margin_levels, 'b-', linewidth=2)
        ax4.axhline(y=100, color='orange', linestyle='--', label='Margin Call (100%)')
        ax4.axhline(y=50, color='red', linestyle='--', label='Stop Out (50%)')
        ax4.fill_between(price_changes, margin_levels, 0, 
                        where=margin_levels < 50, alpha=0.3, color='red')
        ax4.fill_between(price_changes, margin_levels, 0,
                        where=(margin_levels >= 50) & (margin_levels < 100), 
                        alpha=0.3, color='orange')
        ax4.set_xlabel('Price Change (%)')
        ax4.set_ylabel('Margin Level (%)')
        ax4.set_title('Margin Level vs Price Change')
        ax4.legend()
        ax4.grid(True, alpha=0.3)
        ax4.set_ylim(0, 500)
        
        plt.tight_layout()
        plt.show()
    
    # ==================== Summary ====================
    
    def print_analysis(
        self,
        stop_loss_pips: int = 50,
        entry_price: float = 1.0850
    ):
        """Print comprehensive analysis."""
        print("\n" + "=" * 70)
        print("LEVERAGE RISK ANALYSIS")
        print("=" * 70)
        
        print(f"\nAccount Balance: ${self.account_balance:,}")
        print(f"Max Leverage: {self.max_leverage}:1")
        print(f"Risk Per Trade: {self.risk_per_trade}%")
        
        # Position sizing
        print("\n" + "-" * 70)
        print("POSITION SIZING")
        print("-" * 70)
        sizing = self.calculate_position_size(stop_loss_pips)
        for k, v in sizing.items():
            print(f"  {k}: {v}")
        
        # Margin call levels
        print("\n" + "-" * 70)
        print("MARGIN CALL ANALYSIS")
        print("-" * 70)
        margin_info = self.calculate_margin_call_level(
            entry_price, sizing['position_value'], 'long'
        )
        for k, v in margin_info.items():
            print(f"  {k}: {v}")
        
        # Risk metrics
        print("\n" + "-" * 70)
        print("RISK METRICS")
        print("-" * 70)
        metrics = self.calculate_risk_metrics(sizing['position_value'])
        for k, v in metrics.items():
            print(f"  {k}: {v}")
        
        # Scenario analysis
        print("\n" + "-" * 70)
        print("SCENARIO ANALYSIS")
        print("-" * 70)
        scenarios = self.scenario_analysis(sizing['position_value'])
        print(scenarios.to_string(index=False))
        
        print("\n" + "=" * 70)
# Demonstrate the Leverage Risk Manager
manager = LeverageRiskManager(
    account_balance=10000,
    max_leverage=50,
    risk_per_trade=2.0,
    max_portfolio_risk=10.0
)

# Print comprehensive analysis
manager.print_analysis(stop_loss_pips=50, entry_price=1.0850)
# Visualize risk profile
manager.plot_risk_profile(position_value=100000)

Key Takeaways

  • Leverage amplifies both gains and losses - a 1% price move with 50:1 leverage is a 50% account change
  • Margin is the collateral required to open and maintain positions
  • Initial margin opens positions; maintenance margin keeps them open
  • Margin calls occur when equity falls below maintenance level - action required or positions get liquidated
  • Position sizing should be based on risk (% of account), not margin available
  • Professional traders typically use 5:1 to 15:1 effective leverage, not the maximum available
  • Higher leverage = smaller adverse move to wipeout (50:1 = 2% move, 100:1 = 1% move)
  • Recovery math is brutal - a 50% loss requires a 100% gain to recover

Next: Module 4 - Data & Tools

Module 4: Data & Tools

Part 1: Market Fundamentals

Duration Exercises
~2.5 hours 6

Learning Objectives

By the end of this module, you will be able to:

  • Access forex data from multiple sources
  • Set up and use the OANDA API for forex data
  • Download and store historical price data
  • Work with real-time streaming data
  • Build robust data pipelines for forex trading
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Tuple, Generator
from dataclasses import dataclass
import json
import time
from pathlib import Path

# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')

4.1 Forex Data Sources

Available Data Sources

Source Type Cost Best For
OANDA Broker API Free (demo) Live trading, historical
FXCM Broker API Free (demo) Historical tick data
Alpha Vantage Data API Free tier Basic forex data
Yahoo Finance Data API Free Currency pair proxies
Dukascopy Data Download Free Tick data backtesting
TradingView Charts Free/Paid Charting, basic data

Data Types

  • Tick Data: Every price change (bid/ask)
  • OHLC Bars: Open, High, Low, Close aggregated by time
  • Order Book: Depth of market (advanced)
@dataclass
class ForexDataSource:
    """
    Represents a forex data source.
    
    Attributes:
        name: Source name
        api_type: Type of API/access
        data_types: Available data types
        free_tier: Whether free access is available
    """
    name: str
    api_type: str
    data_types: List[str]
    free_tier: bool
    url: str


# Define available data sources
DATA_SOURCES = {
    'oanda': ForexDataSource(
        name='OANDA',
        api_type='REST/Streaming',
        data_types=['OHLC', 'Tick', 'Order Book'],
        free_tier=True,
        url='https://developer.oanda.com'
    ),
    'fxcm': ForexDataSource(
        name='FXCM',
        api_type='REST/Socket',
        data_types=['OHLC', 'Tick'],
        free_tier=True,
        url='https://fxcm.github.io/rest-api-docs/'
    ),
    'alpha_vantage': ForexDataSource(
        name='Alpha Vantage',
        api_type='REST',
        data_types=['OHLC'],
        free_tier=True,
        url='https://www.alphavantage.co'
    ),
    'yahoo': ForexDataSource(
        name='Yahoo Finance',
        api_type='REST (yfinance)',
        data_types=['OHLC'],
        free_tier=True,
        url='https://finance.yahoo.com'
    ),
    'dukascopy': ForexDataSource(
        name='Dukascopy',
        api_type='Download',
        data_types=['Tick', 'OHLC'],
        free_tier=True,
        url='https://www.dukascopy.com/swiss/english/marketwatch/historical/'
    )
}

# Display sources
print("Forex Data Sources")
print("=" * 70)
for key, source in DATA_SOURCES.items():
    print(f"\n{source.name}")
    print(f"  API Type: {source.api_type}")
    print(f"  Data Types: {', '.join(source.data_types)}")
    print(f"  Free Tier: {'Yes' if source.free_tier else 'No'}")
    print(f"  URL: {source.url}")
# Mock forex data generator (simulates API response)
class MockForexDataProvider:
    """
    Mock data provider for demonstration.
    
    In production, this would be replaced with actual API calls.
    """
    
    # Base prices for common pairs
    BASE_PRICES = {
        'EUR_USD': 1.0850,
        'GBP_USD': 1.2650,
        'USD_JPY': 150.50,
        'AUD_USD': 0.6550,
        'USD_CAD': 1.3650,
        'EUR_GBP': 0.8580,
        'EUR_JPY': 163.30
    }
    
    # Typical spreads in pips
    TYPICAL_SPREADS = {
        'EUR_USD': 0.8,
        'GBP_USD': 1.2,
        'USD_JPY': 1.0,
        'AUD_USD': 1.0,
        'USD_CAD': 1.5,
        'EUR_GBP': 1.5,
        'EUR_JPY': 1.8
    }
    
    def __init__(self, seed: int = 42):
        self.rng = np.random.default_rng(seed)
    
    def get_current_price(self, pair: str) -> dict:
        """
        Get current bid/ask price for a pair.
        
        Args:
            pair: Currency pair (e.g., 'EUR_USD')
            
        Returns:
            Price dictionary with bid, ask, spread
        """
        pair = pair.upper().replace('/', '_')
        base_price = self.BASE_PRICES.get(pair, 1.0)
        spread_pips = self.TYPICAL_SPREADS.get(pair, 2.0)
        
        # Add some randomness
        pip_size = 0.01 if 'JPY' in pair else 0.0001
        noise = self.rng.normal(0, 5) * pip_size
        mid_price = base_price + noise
        
        half_spread = (spread_pips / 2) * pip_size
        
        return {
            'pair': pair,
            'bid': round(mid_price - half_spread, 5),
            'ask': round(mid_price + half_spread, 5),
            'mid': round(mid_price, 5),
            'spread_pips': spread_pips,
            'timestamp': datetime.now(timezone.utc).isoformat()
        }
    
    def get_historical_data(
        self,
        pair: str,
        granularity: str = 'H1',
        count: int = 100
    ) -> pd.DataFrame:
        """
        Get historical OHLC data.
        
        Args:
            pair: Currency pair
            granularity: Timeframe (M1, M5, M15, H1, H4, D)
            count: Number of candles
            
        Returns:
            DataFrame with OHLC data
        """
        pair = pair.upper().replace('/', '_')
        base_price = self.BASE_PRICES.get(pair, 1.0)
        pip_size = 0.01 if 'JPY' in pair else 0.0001
        
        # Determine frequency
        freq_map = {
            'M1': '1min', 'M5': '5min', 'M15': '15min',
            'H1': '1h', 'H4': '4h', 'D': '1D'
        }
        freq = freq_map.get(granularity, '1h')
        
        # Generate timestamps
        end_time = datetime.now(timezone.utc)
        dates = pd.date_range(end=end_time, periods=count, freq=freq)
        
        # Generate price path
        returns = self.rng.normal(0, 0.0002, count)  # Small returns
        close_prices = base_price * np.cumprod(1 + returns)
        
        # Generate OHLC
        volatility = 20 * pip_size  # Typical range
        
        df = pd.DataFrame({
            'open': np.roll(close_prices, 1),
            'high': close_prices + self.rng.uniform(0, volatility, count),
            'low': close_prices - self.rng.uniform(0, volatility, count),
            'close': close_prices,
            'volume': self.rng.integers(1000, 10000, count)
        }, index=dates)
        
        df.iloc[0, 0] = base_price  # Fix first open
        
        # Ensure OHLC relationships
        df['high'] = df[['open', 'high', 'close']].max(axis=1)
        df['low'] = df[['open', 'low', 'close']].min(axis=1)
        
        return df.round(5)


# Demonstrate the mock provider
provider = MockForexDataProvider()

print("Current Prices")
print("=" * 60)
for pair in ['EUR_USD', 'GBP_USD', 'USD_JPY']:
    price = provider.get_current_price(pair)
    print(f"{price['pair']:10} Bid: {price['bid']:.5f} | Ask: {price['ask']:.5f} | "
          f"Spread: {price['spread_pips']} pips")

4.2 OANDA API Setup

OANDA provides one of the most popular APIs for forex trading. Here's how to set it up:

Account Setup

  1. Create a demo account at OANDA
  2. Navigate to "Manage API Access" in your account settings
  3. Generate an API token
  4. Note your account ID

API Endpoints

Endpoint Purpose
/v3/accounts Account information
/v3/instruments/{pair}/candles Historical OHLC
/v3/accounts/{id}/pricing/stream Real-time prices
/v3/accounts/{id}/orders Order management
class OANDAClient:
    """
    OANDA API client for forex data and trading.
    
    This is a mock implementation for educational purposes.
    In production, use the oandapyV20 library.
    
    Attributes:
        account_id: OANDA account ID
        access_token: API access token
        environment: 'practice' or 'live'
    """
    
    ENVIRONMENTS = {
        'practice': 'https://api-fxpractice.oanda.com',
        'live': 'https://api-fxtrade.oanda.com'
    }
    
    GRANULARITIES = {
        'S5': 'S5', 'S10': 'S10', 'S15': 'S15', 'S30': 'S30',
        'M1': 'M1', 'M2': 'M2', 'M4': 'M4', 'M5': 'M5',
        'M10': 'M10', 'M15': 'M15', 'M30': 'M30',
        'H1': 'H1', 'H2': 'H2', 'H3': 'H3', 'H4': 'H4',
        'H6': 'H6', 'H8': 'H8', 'H12': 'H12',
        'D': 'D', 'W': 'W', 'M': 'M'
    }
    
    def __init__(
        self,
        account_id: str,
        access_token: str,
        environment: str = 'practice'
    ):
        self.account_id = account_id
        self.access_token = access_token
        self.environment = environment
        self.base_url = self.ENVIRONMENTS[environment]
        
        # Use mock provider for demonstration
        self._mock = MockForexDataProvider()
    
    def _make_request(self, endpoint: str, params: dict = None) -> dict:
        """
        Make API request (mock implementation).
        
        In production, this would use requests library.
        """
        # Mock response based on endpoint
        return {'status': 'ok'}
    
    def get_account_summary(self) -> dict:
        """
        Get account summary.
        
        Returns:
            Account details including balance, margin, etc.
        """
        # Mock account summary
        return {
            'account_id': self.account_id,
            'balance': 10000.00,
            'unrealized_pl': 125.50,
            'nav': 10125.50,
            'margin_used': 500.00,
            'margin_available': 9625.50,
            'open_positions': 2,
            'open_trades': 3
        }
    
    def get_instruments(self) -> List[dict]:
        """
        Get available trading instruments.
        
        Returns:
            List of tradeable instruments
        """
        instruments = [
            {'name': 'EUR_USD', 'type': 'CURRENCY', 'pip': 0.0001},
            {'name': 'GBP_USD', 'type': 'CURRENCY', 'pip': 0.0001},
            {'name': 'USD_JPY', 'type': 'CURRENCY', 'pip': 0.01},
            {'name': 'AUD_USD', 'type': 'CURRENCY', 'pip': 0.0001},
            {'name': 'USD_CAD', 'type': 'CURRENCY', 'pip': 0.0001},
            {'name': 'EUR_GBP', 'type': 'CURRENCY', 'pip': 0.0001},
            {'name': 'EUR_JPY', 'type': 'CURRENCY', 'pip': 0.01},
            {'name': 'XAU_USD', 'type': 'METAL', 'pip': 0.01},
        ]
        return instruments
    
    def get_pricing(self, instruments: List[str]) -> List[dict]:
        """
        Get current pricing for instruments.
        
        Args:
            instruments: List of instrument names
            
        Returns:
            List of price dictionaries
        """
        prices = []
        for inst in instruments:
            prices.append(self._mock.get_current_price(inst))
        return prices
    
    def get_candles(
        self,
        instrument: str,
        granularity: str = 'H1',
        count: int = 100,
        from_time: datetime = None,
        to_time: datetime = None
    ) -> pd.DataFrame:
        """
        Get historical candle data.
        
        Args:
            instrument: Currency pair
            granularity: Timeframe
            count: Number of candles
            from_time: Start time
            to_time: End time
            
        Returns:
            DataFrame with OHLC data
        """
        return self._mock.get_historical_data(instrument, granularity, count)


# Demonstrate OANDA client
client = OANDAClient(
    account_id='101-001-12345678-001',
    access_token='your-api-token-here',
    environment='practice'
)

print("OANDA Account Summary")
print("=" * 50)
summary = client.get_account_summary()
for k, v in summary.items():
    print(f"{k}: {v}")

print("\nAvailable Instruments")
print("=" * 50)
instruments = client.get_instruments()
for inst in instruments[:5]:
    print(f"{inst['name']:10} Type: {inst['type']:10} Pip: {inst['pip']}")

Exercise 4.1: Connect to OANDA (Guided)

Complete the function to set up an OANDA connection and fetch basic data.

Exercise
Solution 4.1
def setup_oanda_connection(
    account_id: str,
    access_token: str,
    environment: str = 'practice'
) -> dict:
    """
    Set up OANDA connection and verify access.
    """
    # Create client
    client = OANDAClient(
        account_id=account_id,
        access_token=access_token,
        environment=environment  # Pass the environment parameter
    )

    # Get account summary
    account = client.get_account_summary()  # Call get_account_summary method

    # Get available instruments
    instruments = client.get_instruments()

    # Get sample pricing
    sample_pairs = ['EUR_USD', 'GBP_USD', 'USD_JPY']
    prices = client.get_pricing(sample_pairs)  # Call get_pricing method

    return {
        'status': 'connected',
        'environment': environment,
        'account_id': account_id,
        'balance': account['balance'],
        'nav': account['nav'],
        'instruments_available': len(instruments),
        'sample_prices': prices
    }

4.3 Historical Data

Downloading and Storing Historical Data

For backtesting and analysis, you need historical price data. Key considerations:

  • Timeframe: Choose appropriate granularity (M1 for scalping, H1 for swing)
  • Date Range: Ensure sufficient history for your strategy
  • Data Quality: Check for gaps, outliers, weekend data
  • Storage Format: CSV, Parquet, or database
class ForexDataManager:
    """
    Manages downloading, storing, and retrieving forex data.
    
    Attributes:
        data_dir: Directory for storing data
        client: OANDA client for fetching data
    """
    
    def __init__(self, data_dir: str = './forex_data', client: OANDAClient = None):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)
        self.client = client or OANDAClient('demo', 'demo')
    
    def _get_file_path(self, pair: str, granularity: str) -> Path:
        """Generate file path for storing data."""
        pair = pair.upper().replace('/', '_')
        return self.data_dir / f"{pair}_{granularity}.csv"
    
    def download_data(
        self,
        pair: str,
        granularity: str = 'H1',
        count: int = 5000
    ) -> pd.DataFrame:
        """
        Download historical data from OANDA.
        
        Args:
            pair: Currency pair
            granularity: Timeframe
            count: Number of candles
            
        Returns:
            Downloaded data as DataFrame
        """
        print(f"Downloading {pair} {granularity} data...")
        df = self.client.get_candles(pair, granularity, count)
        
        # Save to file
        file_path = self._get_file_path(pair, granularity)
        df.to_csv(file_path)
        print(f"Saved {len(df)} candles to {file_path}")
        
        return df
    
    def load_data(self, pair: str, granularity: str = 'H1') -> pd.DataFrame:
        """
        Load data from local storage.
        
        Args:
            pair: Currency pair
            granularity: Timeframe
            
        Returns:
            DataFrame or None if not found
        """
        file_path = self._get_file_path(pair, granularity)
        
        if file_path.exists():
            df = pd.read_csv(file_path, index_col=0, parse_dates=True)
            return df
        else:
            print(f"Data not found: {file_path}")
            return None
    
    def update_data(self, pair: str, granularity: str = 'H1') -> pd.DataFrame:
        """
        Update existing data with latest candles.
        
        Args:
            pair: Currency pair
            granularity: Timeframe
            
        Returns:
            Updated DataFrame
        """
        existing = self.load_data(pair, granularity)
        
        if existing is not None:
            # Get new data from last timestamp
            new_data = self.client.get_candles(pair, granularity, 100)
            
            # Combine and remove duplicates
            combined = pd.concat([existing, new_data])
            combined = combined[~combined.index.duplicated(keep='last')]
            combined = combined.sort_index()
            
            # Save
            file_path = self._get_file_path(pair, granularity)
            combined.to_csv(file_path)
            
            return combined
        else:
            return self.download_data(pair, granularity)
    
    def get_available_data(self) -> List[dict]:
        """
        List all available local data.
        
        Returns:
            List of available datasets
        """
        available = []
        for file_path in self.data_dir.glob('*.csv'):
            parts = file_path.stem.split('_')
            if len(parts) >= 3:
                pair = f"{parts[0]}_{parts[1]}"
                granularity = parts[2]
                
                # Get file info
                df = pd.read_csv(file_path, index_col=0, parse_dates=True)
                available.append({
                    'pair': pair,
                    'granularity': granularity,
                    'candles': len(df),
                    'start': df.index[0].strftime('%Y-%m-%d'),
                    'end': df.index[-1].strftime('%Y-%m-%d'),
                    'file': file_path.name
                })
        return available
    
    def validate_data(self, df: pd.DataFrame) -> dict:
        """
        Validate data quality.
        
        Args:
            df: DataFrame to validate
            
        Returns:
            Validation report
        """
        issues = []
        
        # Check for missing values
        missing = df.isnull().sum()
        if missing.any():
            issues.append(f"Missing values: {missing.to_dict()}")
        
        # Check OHLC relationships
        invalid_hl = df[df['high'] < df['low']]
        if len(invalid_hl) > 0:
            issues.append(f"Invalid H/L: {len(invalid_hl)} rows")
        
        invalid_oh = df[(df['open'] > df['high']) | (df['open'] < df['low'])]
        if len(invalid_oh) > 0:
            issues.append(f"Open outside H/L: {len(invalid_oh)} rows")
        
        invalid_ch = df[(df['close'] > df['high']) | (df['close'] < df['low'])]
        if len(invalid_ch) > 0:
            issues.append(f"Close outside H/L: {len(invalid_ch)} rows")
        
        # Check for gaps (more than expected frequency)
        if len(df) > 1:
            time_diffs = df.index.to_series().diff().dropna()
            median_diff = time_diffs.median()
            large_gaps = time_diffs[time_diffs > median_diff * 5]
            if len(large_gaps) > 0:
                issues.append(f"Large gaps: {len(large_gaps)} (expected for weekends)")
        
        return {
            'valid': len(issues) == 0,
            'rows': len(df),
            'date_range': f"{df.index[0]} to {df.index[-1]}",
            'issues': issues if issues else ['No issues found']
        }


# Demonstrate data manager
manager = ForexDataManager('./demo_data')

# Download sample data
eur_usd = manager.download_data('EUR_USD', 'H1', 500)
print(f"\nDownloaded data shape: {eur_usd.shape}")
print(eur_usd.tail())
# Validate the downloaded data
validation = manager.validate_data(eur_usd)
print("\nData Validation Report")
print("=" * 50)
for k, v in validation.items():
    print(f"{k}: {v}")

Exercise 4.2: Build Data Pipeline (Guided)

Complete the function to build a data download pipeline for multiple pairs and timeframes.

Exercise
Solution 4.2
def build_data_pipeline(
    pairs: List[str],
    timeframes: List[str],
    data_dir: str = './pipeline_data'
) -> dict:
    """
    Build a data pipeline for multiple pairs and timeframes.
    """
    manager = ForexDataManager(data_dir)

    downloaded = []
    failed = []
    total_candles = 0

    # Iterate through all combinations
    for pair in pairs:  # Iterate through pairs
        for tf in timeframes:  # Iterate through timeframes
            try:
                # Download data
                df = manager.download_data(pair, tf, 500)  # Call download method

                downloaded.append({
                    'pair': pair,
                    'timeframe': tf,
                    'candles': len(df)
                })
                total_candles += len(df)

            except Exception as e:
                failed.append({
                    'pair': pair,
                    'timeframe': tf,
                    'error': str(e)
                })

    return {
        'total_downloads': len(downloaded),
        'total_candles': total_candles,
        'failed': len(failed),
        'downloaded': downloaded,
        'failures': failed
    }

4.4 Real-Time Data

Streaming Price Data

For live trading, you need real-time streaming prices. OANDA provides a streaming endpoint that pushes price updates as they occur.

class MockPriceStream:
    """
    Simulates a real-time price stream.
    
    In production, this would connect to OANDA's streaming API.
    """
    
    def __init__(self, instruments: List[str], seed: int = 42):
        self.instruments = [i.upper().replace('/', '_') for i in instruments]
        self.rng = np.random.default_rng(seed)
        self._provider = MockForexDataProvider(seed)
        self._running = False
        self._prices = {}  # Current prices
        
        # Initialize prices
        for inst in self.instruments:
            self._prices[inst] = self._provider.get_current_price(inst)
    
    def _update_prices(self):
        """Simulate price updates."""
        for inst in self.instruments:
            current = self._prices[inst]
            pip_size = 0.01 if 'JPY' in inst else 0.0001
            
            # Random walk
            change = self.rng.normal(0, 2) * pip_size
            new_mid = current['mid'] + change
            half_spread = (current['spread_pips'] / 2) * pip_size
            
            self._prices[inst] = {
                'pair': inst,
                'bid': round(new_mid - half_spread, 5),
                'ask': round(new_mid + half_spread, 5),
                'mid': round(new_mid, 5),
                'spread_pips': current['spread_pips'],
                'timestamp': datetime.now(timezone.utc).isoformat()
            }
    
    def stream(self, duration_seconds: int = 10) -> Generator[dict, None, None]:
        """
        Stream price updates.
        
        Args:
            duration_seconds: How long to stream
            
        Yields:
            Price update dictionaries
        """
        self._running = True
        start_time = time.time()
        
        while self._running and (time.time() - start_time) < duration_seconds:
            self._update_prices()
            
            # Yield random instrument update
            inst = self.rng.choice(self.instruments)
            yield self._prices[inst]
            
            # Simulate network delay
            time.sleep(0.1)
    
    def stop(self):
        """Stop the stream."""
        self._running = False
    
    def get_current_prices(self) -> Dict[str, dict]:
        """Get current prices for all instruments."""
        return self._prices.copy()


# Demonstrate streaming
stream = MockPriceStream(['EUR_USD', 'GBP_USD', 'USD_JPY'])

print("Simulated Price Stream (5 updates)")
print("=" * 70)

count = 0
for price in stream.stream(duration_seconds=2):
    print(f"[{price['timestamp'][-12:-1]}] {price['pair']:10} "
          f"Bid: {price['bid']:.5f} | Ask: {price['ask']:.5f}")
    count += 1
    if count >= 5:
        stream.stop()
        break
class RealTimeDataHandler:
    """
    Handles real-time forex data processing.
    
    Attributes:
        stream: Price stream source
        buffer_size: Number of ticks to buffer
    """
    
    def __init__(self, instruments: List[str], buffer_size: int = 100):
        self.instruments = instruments
        self.buffer_size = buffer_size
        self.stream = MockPriceStream(instruments)
        
        # Initialize buffers for each instrument
        self.tick_buffers = {inst: [] for inst in instruments}
        self.callbacks = []  # List of callback functions
    
    def add_callback(self, callback):
        """Add a callback function to be called on each tick."""
        self.callbacks.append(callback)
    
    def _process_tick(self, tick: dict):
        """Process a single tick."""
        pair = tick['pair']
        
        # Add to buffer
        if pair in self.tick_buffers:
            self.tick_buffers[pair].append(tick)
            
            # Trim buffer if too large
            if len(self.tick_buffers[pair]) > self.buffer_size:
                self.tick_buffers[pair] = self.tick_buffers[pair][-self.buffer_size:]
        
        # Call callbacks
        for callback in self.callbacks:
            callback(tick)
    
    def run(self, duration: int = 10):
        """Run the data handler."""
        print(f"Starting data handler for {duration} seconds...")
        
        for tick in self.stream.stream(duration):
            self._process_tick(tick)
        
        print("Data handler stopped.")
    
    def get_tick_data(self, instrument: str) -> pd.DataFrame:
        """Get buffered tick data as DataFrame."""
        inst = instrument.upper().replace('/', '_')
        if inst not in self.tick_buffers:
            return pd.DataFrame()
        
        ticks = self.tick_buffers[inst]
        if not ticks:
            return pd.DataFrame()
        
        df = pd.DataFrame(ticks)
        df['timestamp'] = pd.to_datetime(df['timestamp'])
        df = df.set_index('timestamp')
        return df
    
    def get_latest_prices(self) -> Dict[str, dict]:
        """Get latest price for each instrument."""
        latest = {}
        for inst, buffer in self.tick_buffers.items():
            if buffer:
                latest[inst] = buffer[-1]
        return latest


# Demonstrate real-time handler
handler = RealTimeDataHandler(['EUR_USD', 'GBP_USD'], buffer_size=50)

# Add a simple callback
tick_count = [0]
def count_ticks(tick):
    tick_count[0] += 1

handler.add_callback(count_ticks)

# Run for short duration
handler.run(duration=2)

print(f"\nTotal ticks received: {tick_count[0]}")
print(f"\nLatest Prices:")
for pair, price in handler.get_latest_prices().items():
    print(f"  {pair}: {price['mid']:.5f}")

Exercise 4.3: Real-Time Feed (Guided)

Complete the function to process real-time price updates and calculate simple metrics.

Exercise
Solution 4.3
def process_price_stream(
    instruments: List[str],
    duration: int = 5,
    alert_threshold_pips: float = 3.0
) -> dict:
    """
    Process a price stream and generate statistics.
    """
    stream = MockPriceStream(instruments)

    stats = {}
    alerts = []
    first_prices = {}

    for tick in stream.stream(duration):
        pair = tick['pair']
        mid = tick['mid']
        pip_size = 0.01 if 'JPY' in pair else 0.0001

        # Initialize stats for pair
        if pair not in stats:
            stats[pair] = {
                'ticks': 0,
                'prices': [],
                'first': mid,
                'last': mid
            }
            first_prices[pair] = mid

        # Update stats
        stats[pair]['ticks'] += 1
        stats[pair]['prices'].append(mid)  # Append price to list
        stats[pair]['last'] = mid

        # Check for significant move
        move_pips = abs(mid - first_prices[pair]) / pip_size  # Convert to pips

        if move_pips >= alert_threshold_pips:
            direction = 'UP' if mid > first_prices[pair] else 'DOWN'  # Determine direction
            alerts.append({
                'pair': pair,
                'move_pips': round(move_pips, 1),
                'direction': direction,
                'timestamp': tick['timestamp']
            })

    # Calculate final statistics
    summary = {}
    for pair, data in stats.items():
        pip_size = 0.01 if 'JPY' in pair else 0.0001
        prices = data['prices']

        summary[pair] = {
            'tick_count': data['ticks'],
            'high': max(prices) if prices else 0,
            'low': min(prices) if prices else 0,
            'range_pips': round((max(prices) - min(prices)) / pip_size, 1) if prices else 0,
            'net_change_pips': round((data['last'] - data['first']) / pip_size, 1)
        }

    return {
        'duration': duration,
        'instruments': instruments,
        'summary': summary,
        'alerts': alerts
    }

Exercise 4.4: Data Source Manager (Open-ended)

Build a multi-source data manager that can fetch data from different providers.

Your implementation:

Exercise
Solution 4.4
class MultiSourceDataManager:
    """
    Manages multiple forex data sources with fallback.
    """

    def __init__(self, cache_dir: str = './cache'):
        self.sources = {}  # name -> {provider, priority, status}
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self.cache = {}  # (pair, timeframe) -> (data, timestamp)
        self.cache_ttl = 3600  # 1 hour cache

    def add_source(self, name: str, provider, priority: int = 1):
        """Add a data source with priority (lower = higher priority)."""
        self.sources[name] = {
            'provider': provider,
            'priority': priority,
            'status': 'active',
            'failures': 0
        }

    def _get_sorted_sources(self) -> List[Tuple[str, dict]]:
        """Get sources sorted by priority."""
        active = [(n, s) for n, s in self.sources.items() if s['status'] == 'active']
        return sorted(active, key=lambda x: x[1]['priority'])

    def _check_cache(self, pair: str, timeframe: str) -> Optional[pd.DataFrame]:
        """Check if data is in cache and not expired."""
        key = (pair, timeframe)
        if key in self.cache:
            data, timestamp = self.cache[key]
            if time.time() - timestamp < self.cache_ttl:
                return data
        return None

    def _update_cache(self, pair: str, timeframe: str, data: pd.DataFrame):
        """Update cache with new data."""
        self.cache[(pair, timeframe)] = (data, time.time())

    def fetch_data(self, pair: str, timeframe: str, source_name: str = None) -> pd.DataFrame:
        """Fetch data from a specific source."""
        if source_name is None:
            sources = self._get_sorted_sources()
            source_name = sources[0][0] if sources else None

        if source_name not in self.sources:
            raise ValueError(f"Source {source_name} not found")

        source = self.sources[source_name]
        provider = source['provider']

        try:
            data = provider.get_candles(pair, timeframe, 100)
            source['failures'] = 0
            self._update_cache(pair, timeframe, data)
            return data
        except Exception as e:
            source['failures'] += 1
            if source['failures'] >= 3:
                source['status'] = 'disabled'
            raise

    def get_with_fallback(self, pair: str, timeframe: str) -> pd.DataFrame:
        """Get data with automatic fallback."""
        # Check cache first
        cached = self._check_cache(pair, timeframe)
        if cached is not None:
            return cached

        # Try sources in priority order
        for name, source in self._get_sorted_sources():
            try:
                return self.fetch_data(pair, timeframe, name)
            except Exception as e:
                print(f"Source {name} failed: {e}")
                continue

        raise Exception("All sources failed")

    def validate_source(self, name: str) -> dict:
        """Validate a data source."""
        if name not in self.sources:
            return {'valid': False, 'error': 'Source not found'}

        source = self.sources[name]
        try:
            data = source['provider'].get_candles('EUR_USD', 'H1', 10)
            return {
                'valid': True,
                'name': name,
                'status': source['status'],
                'sample_rows': len(data),
                'failures': source['failures']
            }
        except Exception as e:
            return {'valid': False, 'error': str(e)}


# Test
manager = MultiSourceDataManager()

# Add sources
manager.add_source('oanda', OANDAClient('demo', 'demo'), priority=1)
manager.add_source('backup', OANDAClient('demo', 'demo'), priority=2)

# Fetch with fallback
data = manager.get_with_fallback('EUR_USD', 'H1')
print(f"Fetched {len(data)} rows")

# Validate sources
for name in manager.sources:
    print(f"\n{name}: {manager.validate_source(name)}")

Exercise 4.5: Streaming Data Aggregator (Open-ended)

Build a streaming data aggregator that converts tick data to OHLC bars.

Your implementation:

Exercise
Solution 4.5
class StreamingAggregator:
    """
    Aggregates tick data into OHLC bars.
    """

    TIMEFRAME_SECONDS = {
        'M1': 60, 'M5': 300, 'M15': 900, 'M30': 1800,
        'H1': 3600, 'H4': 14400, 'D': 86400
    }

    def __init__(self):
        self.timeframes = {}  # timeframe -> {bars, current_bar, callback}

    def add_timeframe(self, timeframe: str, callback: callable = None):
        """Add a timeframe to aggregate."""
        self.timeframes[timeframe] = {
            'seconds': self.TIMEFRAME_SECONDS.get(timeframe, 3600),
            'bars': [],
            'current_bar': None,
            'callback': callback
        }

    def _get_bar_timestamp(self, tick_time: datetime, seconds: int) -> datetime:
        """Get the bar start timestamp for a tick."""
        epoch = tick_time.timestamp()
        bar_epoch = (epoch // seconds) * seconds
        return datetime.fromtimestamp(bar_epoch, tz=timezone.utc)

    def process_tick(self, tick: dict):
        """Process a single tick."""
        tick_time = datetime.fromisoformat(tick['timestamp'].replace('Z', '+00:00'))
        mid = tick['mid']

        for tf, data in self.timeframes.items():
            bar_time = self._get_bar_timestamp(tick_time, data['seconds'])

            # Check if new bar needed
            if data['current_bar'] is None or data['current_bar']['timestamp'] != bar_time:
                # Complete previous bar
                if data['current_bar'] is not None:
                    data['bars'].append(data['current_bar'])
                    if data['callback']:
                        data['callback'](tf, data['current_bar'])

                # Start new bar
                data['current_bar'] = {
                    'timestamp': bar_time,
                    'open': mid,
                    'high': mid,
                    'low': mid,
                    'close': mid,
                    'tick_count': 1
                }
            else:
                # Update current bar
                bar = data['current_bar']
                bar['high'] = max(bar['high'], mid)
                bar['low'] = min(bar['low'], mid)
                bar['close'] = mid
                bar['tick_count'] += 1

    def get_current_bar(self, timeframe: str) -> dict:
        """Get the current (incomplete) bar."""
        if timeframe not in self.timeframes:
            return None
        return self.timeframes[timeframe]['current_bar']

    def get_completed_bars(self, timeframe: str) -> List[dict]:
        """Get all completed bars."""
        if timeframe not in self.timeframes:
            return []
        return self.timeframes[timeframe]['bars']

    def to_dataframe(self, timeframe: str) -> pd.DataFrame:
        """Convert completed bars to DataFrame."""
        bars = self.get_completed_bars(timeframe)
        if not bars:
            return pd.DataFrame()

        df = pd.DataFrame(bars)
        df = df.set_index('timestamp')
        return df


# Test aggregator
aggregator = StreamingAggregator()

# Add callback
def on_bar_complete(tf, bar):
    print(f"New {tf} bar: O={bar['open']:.5f} H={bar['high']:.5f} "
          f"L={bar['low']:.5f} C={bar['close']:.5f}")

aggregator.add_timeframe('M1', callback=on_bar_complete)

# Process some ticks
stream = MockPriceStream(['EUR_USD'])
for tick in stream.stream(duration=5):
    aggregator.process_tick(tick)

print(f"\nCompleted bars: {len(aggregator.get_completed_bars('M1'))}")
print(f"Current bar: {aggregator.get_current_bar('M1')}")

Exercise 4.6: Forex Data System (Open-ended)

Build a complete forex data system that combines all the components from this module.

Your implementation:

Exercise
Solution 4.6
class ForexDataSystem:
    """
    Complete forex data management system.
    """

    def __init__(self, data_dir: str = './forex_system'):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)

        self.client = OANDAClient('demo', 'demo')
        self.data_manager = ForexDataManager(str(self.data_dir))
        self.stream = None
        self.aggregator = StreamingAggregator()

        self.instruments = []
        self.timeframes = []
        self.is_streaming = False
        self.latest_prices = {}

    def initialize(self, instruments: List[str], timeframes: List[str]):
        """Initialize the system."""
        self.instruments = [i.upper().replace('/', '_') for i in instruments]
        self.timeframes = timeframes

        # Set up aggregator
        for tf in timeframes:
            self.aggregator.add_timeframe(tf)

        print(f"Initialized with {len(instruments)} instruments, {len(timeframes)} timeframes")

    def download_history(self, start_date: datetime = None, end_date: datetime = None):
        """Download historical data."""
        for pair in self.instruments:
            for tf in self.timeframes:
                print(f"Downloading {pair} {tf}...")
                self.data_manager.download_data(pair, tf, 1000)

    def start_streaming(self):
        """Start streaming prices."""
        self.is_streaming = True
        self.stream = MockPriceStream(self.instruments)
        print("Streaming started")

    def stop_streaming(self):
        """Stop streaming."""
        self.is_streaming = False
        if self.stream:
            self.stream.stop()
        print("Streaming stopped")

    def process_ticks(self, duration: int = 5):
        """Process streaming ticks."""
        if not self.is_streaming:
            self.start_streaming()

        for tick in self.stream.stream(duration):
            self.aggregator.process_tick(tick)
            self.latest_prices[tick['pair']] = tick

    def get_data(self, instrument: str, timeframe: str) -> pd.DataFrame:
        """Get historical data."""
        inst = instrument.upper().replace('/', '_')
        return self.data_manager.load_data(inst, timeframe)

    def get_latest_price(self, instrument: str) -> dict:
        """Get latest streaming price."""
        inst = instrument.upper().replace('/', '_')
        return self.latest_prices.get(inst)

    def health_check(self) -> dict:
        """Check system health."""
        return {
            'status': 'healthy',
            'instruments': len(self.instruments),
            'timeframes': len(self.timeframes),
            'streaming': self.is_streaming,
            'latest_prices': len(self.latest_prices),
            'data_files': len(list(self.data_dir.glob('*.csv')))
        }

    def summary(self):
        """Print system summary."""
        print("\n" + "=" * 60)
        print("FOREX DATA SYSTEM SUMMARY")
        print("=" * 60)
        health = self.health_check()
        for k, v in health.items():
            print(f"  {k}: {v}")
        print("=" * 60)


# Test the system
system = ForexDataSystem('./test_system')
system.initialize(
    instruments=['EUR_USD', 'GBP_USD', 'USD_JPY'],
    timeframes=['M1', 'H1']
)

# Download history
system.download_history()

# Process some streaming data
system.start_streaming()
system.process_ticks(duration=3)
system.stop_streaming()

# Print summary
system.summary()

# Get latest prices
print("\nLatest Prices:")
for pair in ['EUR_USD', 'GBP_USD']:
    price = system.get_latest_price(pair)
    if price:
        print(f"  {pair}: {price['mid']:.5f}")

Module Project: Forex Data System

Build a production-ready forex data system combining all concepts from this module.

class ProductionForexDataSystem:
    """
    Production-ready forex data management system.
    
    Features:
    - Multi-source data fetching with fallback
    - Historical data management with caching
    - Real-time streaming with tick aggregation
    - Data validation and quality monitoring
    - Health checks and status reporting
    """
    
    def __init__(self, config: dict = None):
        self.config = config or self._default_config()
        self.data_dir = Path(self.config['data_dir'])
        self.data_dir.mkdir(parents=True, exist_ok=True)
        
        # Components
        self.client = OANDAClient(
            self.config.get('account_id', 'demo'),
            self.config.get('access_token', 'demo'),
            self.config.get('environment', 'practice')
        )
        
        self.instruments = []
        self.timeframes = []
        self.historical_data = {}  # (pair, tf) -> DataFrame
        self.latest_prices = {}
        self.stream = None
        self.is_running = False
        
        # Metrics
        self.metrics = {
            'ticks_received': 0,
            'bars_completed': 0,
            'errors': 0,
            'last_update': None
        }
    
    def _default_config(self) -> dict:
        return {
            'data_dir': './forex_data_system',
            'account_id': 'demo',
            'access_token': 'demo',
            'environment': 'practice',
            'default_history_count': 1000,
            'cache_ttl': 3600
        }
    
    # ==================== Initialization ====================
    
    def initialize(
        self,
        instruments: List[str],
        timeframes: List[str] = None
    ):
        """
        Initialize the data system.
        
        Args:
            instruments: List of currency pairs
            timeframes: List of timeframes (default: H1, H4, D)
        """
        self.instruments = [i.upper().replace('/', '_') for i in instruments]
        self.timeframes = timeframes or ['H1', 'H4', 'D']
        
        print(f"Initializing Forex Data System")
        print(f"  Instruments: {len(self.instruments)}")
        print(f"  Timeframes: {self.timeframes}")
    
    # ==================== Historical Data ====================
    
    def download_historical(
        self,
        count: int = None,
        instruments: List[str] = None,
        timeframes: List[str] = None
    ):
        """
        Download historical data for all or specified instruments.
        """
        instruments = instruments or self.instruments
        timeframes = timeframes or self.timeframes
        count = count or self.config['default_history_count']
        
        total = len(instruments) * len(timeframes)
        completed = 0
        
        for pair in instruments:
            for tf in timeframes:
                try:
                    df = self.client.get_candles(pair, tf, count)
                    self.historical_data[(pair, tf)] = df
                    
                    # Save to file
                    file_path = self.data_dir / f"{pair}_{tf}.csv"
                    df.to_csv(file_path)
                    
                    completed += 1
                    print(f"[{completed}/{total}] Downloaded {pair} {tf}: {len(df)} candles")
                    
                except Exception as e:
                    self.metrics['errors'] += 1
                    print(f"Error downloading {pair} {tf}: {e}")
        
        print(f"\nDownload complete: {completed}/{total} successful")
    
    def load_historical(self, pair: str, timeframe: str) -> pd.DataFrame:
        """
        Load historical data (from memory or file).
        """
        pair = pair.upper().replace('/', '_')
        
        # Check memory cache
        if (pair, timeframe) in self.historical_data:
            return self.historical_data[(pair, timeframe)]
        
        # Check file
        file_path = self.data_dir / f"{pair}_{timeframe}.csv"
        if file_path.exists():
            df = pd.read_csv(file_path, index_col=0, parse_dates=True)
            self.historical_data[(pair, timeframe)] = df
            return df
        
        return None
    
    # ==================== Real-Time Data ====================
    
    def start_streaming(self):
        """Start real-time price streaming."""
        self.stream = MockPriceStream(self.instruments)
        self.is_running = True
        print("Streaming started for:", self.instruments)
    
    def stop_streaming(self):
        """Stop streaming."""
        self.is_running = False
        if self.stream:
            self.stream.stop()
        print("Streaming stopped")
    
    def process_stream(self, duration: int = 10, callback=None):
        """
        Process streaming data for a duration.
        
        Args:
            duration: Seconds to stream
            callback: Optional callback for each tick
        """
        if not self.is_running:
            self.start_streaming()
        
        for tick in self.stream.stream(duration):
            self.metrics['ticks_received'] += 1
            self.latest_prices[tick['pair']] = tick
            self.metrics['last_update'] = tick['timestamp']
            
            if callback:
                callback(tick)
    
    def get_latest_price(self, pair: str) -> dict:
        """Get latest price for an instrument."""
        pair = pair.upper().replace('/', '_')
        return self.latest_prices.get(pair)
    
    def get_all_prices(self) -> pd.DataFrame:
        """Get all latest prices as DataFrame."""
        if not self.latest_prices:
            return pd.DataFrame()
        
        return pd.DataFrame(self.latest_prices.values())
    
    # ==================== Data Validation ====================
    
    def validate_data(self, pair: str, timeframe: str) -> dict:
        """Validate data quality."""
        df = self.load_historical(pair, timeframe)
        
        if df is None:
            return {'valid': False, 'error': 'Data not found'}
        
        issues = []
        
        # Check for missing data
        if df.isnull().any().any():
            issues.append('Contains missing values')
        
        # Check OHLC relationships
        if not (df['high'] >= df['low']).all():
            issues.append('High < Low in some rows')
        
        # Check for large gaps
        if len(df) > 1:
            gaps = df.index.to_series().diff().dropna()
            median_gap = gaps.median()
            large_gaps = (gaps > median_gap * 10).sum()
            if large_gaps > 0:
                issues.append(f'{large_gaps} large gaps (weekends expected)')
        
        return {
            'valid': len(issues) == 0,
            'pair': pair,
            'timeframe': timeframe,
            'rows': len(df),
            'date_range': f"{df.index[0]} to {df.index[-1]}",
            'issues': issues if issues else ['No issues']
        }
    
    # ==================== Health & Status ====================
    
    def health_check(self) -> dict:
        """Perform system health check."""
        data_files = list(self.data_dir.glob('*.csv'))
        
        return {
            'status': 'healthy' if self.metrics['errors'] < 10 else 'degraded',
            'streaming': self.is_running,
            'instruments': len(self.instruments),
            'timeframes': len(self.timeframes),
            'data_files': len(data_files),
            'cached_datasets': len(self.historical_data),
            'latest_prices': len(self.latest_prices),
            'ticks_received': self.metrics['ticks_received'],
            'errors': self.metrics['errors'],
            'last_update': self.metrics['last_update']
        }
    
    def print_status(self):
        """Print detailed system status."""
        health = self.health_check()
        
        print("\n" + "=" * 60)
        print("FOREX DATA SYSTEM STATUS")
        print("=" * 60)
        
        print(f"\nSystem Health: {health['status'].upper()}")
        print(f"Streaming: {'Active' if health['streaming'] else 'Stopped'}")
        
        print(f"\nConfiguration:")
        print(f"  Instruments: {health['instruments']}")
        print(f"  Timeframes: {health['timeframes']}")
        
        print(f"\nData:")
        print(f"  Data Files: {health['data_files']}")
        print(f"  Cached Datasets: {health['cached_datasets']}")
        print(f"  Latest Prices: {health['latest_prices']}")
        
        print(f"\nMetrics:")
        print(f"  Ticks Received: {health['ticks_received']}")
        print(f"  Errors: {health['errors']}")
        print(f"  Last Update: {health['last_update']}")
        
        if self.latest_prices:
            print(f"\nLatest Prices:")
            for pair, price in self.latest_prices.items():
                print(f"  {pair}: {price['mid']:.5f} (spread: {price['spread_pips']} pips)")
        
        print("\n" + "=" * 60)
# Demonstrate the Production Forex Data System

# Create and configure system
system = ProductionForexDataSystem({
    'data_dir': './production_forex_data',
    'account_id': 'demo-account',
    'access_token': 'demo-token',
    'environment': 'practice',
    'default_history_count': 500
})

# Initialize
system.initialize(
    instruments=['EUR_USD', 'GBP_USD', 'USD_JPY', 'AUD_USD'],
    timeframes=['H1', 'H4', 'D']
)
# Download historical data
system.download_historical(count=200)
# Validate data quality
print("\nData Validation Results")
print("=" * 50)
for pair in ['EUR_USD', 'GBP_USD']:
    validation = system.validate_data(pair, 'H1')
    print(f"\n{pair} H1:")
    print(f"  Valid: {validation['valid']}")
    print(f"  Rows: {validation['rows']}")
    print(f"  Issues: {validation['issues']}")
# Start streaming and process for a few seconds
print("\nProcessing live stream...")
system.start_streaming()

# Process with a simple callback
def print_tick(tick):
    print(f"  [{tick['timestamp'][-12:-1]}] {tick['pair']}: {tick['mid']:.5f}")

system.process_stream(duration=2, callback=print_tick)
system.stop_streaming()
# Print final status
system.print_status()

Key Takeaways

  • Multiple data sources are available for forex: OANDA, FXCM, Alpha Vantage, Yahoo Finance
  • OANDA API provides free access to historical and real-time forex data via REST and streaming endpoints
  • Historical data management requires proper storage, validation, and update mechanisms
  • Real-time streaming enables live trading but requires tick aggregation for bar-based analysis
  • Data validation is critical - check for gaps, invalid OHLC relationships, and missing values
  • Production systems need health checks, error handling, and fallback data sources
  • Caching reduces API calls and improves performance for frequently accessed data

Next: Part 2 - Analysis & Strategies

Module 5: Technical Analysis for Forex & Futures

Part 2: Analysis & Strategies

Duration Exercises
~2.5 hours 6

Learning Objectives

  • Identify and trade chart patterns in forex markets
  • Build forex-specific indicators like currency strength meters
  • Apply multi-timeframe analysis for better entries
  • Trade using pure price action without indicators

Prerequisites

  • Modules 1-4 (Forex/Futures fundamentals, data tools)
  • Basic understanding of candlestick charts
  • Python pandas and numpy proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')

5.1 Forex Chart Patterns

Chart patterns are visual formations that help identify potential price movements. In forex, these patterns are particularly reliable due to high liquidity.

Support and Resistance Levels

Support and resistance are price levels where buying or selling pressure historically reverses price direction.

class SupportResistanceFinder:
    """Identifies support and resistance levels from price data."""
    
    def __init__(self, lookback: int = 20, touch_threshold: float = 0.001):
        self.lookback = lookback
        self.touch_threshold = touch_threshold
    
    def find_pivot_points(self, df: pd.DataFrame) -> Dict[str, List[float]]:
        """Find swing highs and lows as potential S/R levels."""
        highs = []
        lows = []
        
        for i in range(self.lookback, len(df) - self.lookback):
            window_high = df['high'].iloc[i-self.lookback:i+self.lookback+1]
            if df['high'].iloc[i] == window_high.max():
                highs.append(df['high'].iloc[i])
            
            window_low = df['low'].iloc[i-self.lookback:i+self.lookback+1]
            if df['low'].iloc[i] == window_low.min():
                lows.append(df['low'].iloc[i])
        
        return {'resistance': highs, 'support': lows}
    
    def cluster_levels(self, levels: List[float], tolerance: float = 0.002) -> List[float]:
        """Cluster nearby levels into single stronger levels."""
        if not levels:
            return []
        
        sorted_levels = sorted(levels)
        clusters = [[sorted_levels[0]]]
        
        for level in sorted_levels[1:]:
            if (level - clusters[-1][-1]) / clusters[-1][-1] < tolerance:
                clusters[-1].append(level)
            else:
                clusters.append([level])
        
        return [np.mean(cluster) for cluster in clusters]
    
    def count_touches(self, df: pd.DataFrame, level: float) -> int:
        """Count how many times price touched a level."""
        touches = 0
        for _, row in df.iterrows():
            if abs(row['high'] - level) / level < self.touch_threshold:
                touches += 1
            elif abs(row['low'] - level) / level < self.touch_threshold:
                touches += 1
        return touches
    
    def get_key_levels(self, df: pd.DataFrame, min_touches: int = 2) -> Dict[str, List[Dict]]:
        """Get key S/R levels with touch counts."""
        pivots = self.find_pivot_points(df)
        
        resistance_clustered = self.cluster_levels(pivots['resistance'])
        support_clustered = self.cluster_levels(pivots['support'])
        
        key_levels = {'resistance': [], 'support': []}
        
        for level in resistance_clustered:
            touches = self.count_touches(df, level)
            if touches >= min_touches:
                key_levels['resistance'].append({
                    'level': level,
                    'touches': touches,
                    'strength': 'strong' if touches >= 4 else 'moderate'
                })
        
        for level in support_clustered:
            touches = self.count_touches(df, level)
            if touches >= min_touches:
                key_levels['support'].append({
                    'level': level,
                    'touches': touches,
                    'strength': 'strong' if touches >= 4 else 'moderate'
                })
        
        return key_levels


# Generate demo data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=200, freq='4h')
base_price = 1.0850
prices = [base_price]
for _ in range(199):
    change = np.random.normal(0, 0.002)
    if prices[-1] > 1.0950:
        change -= 0.001
    elif prices[-1] < 1.0750:
        change += 0.001
    prices.append(prices[-1] + change)

demo_df = pd.DataFrame({
    'timestamp': dates,
    'open': prices,
    'high': [p + np.random.uniform(0.001, 0.003) for p in prices],
    'low': [p - np.random.uniform(0.001, 0.003) for p in prices],
    'close': [p + np.random.normal(0, 0.001) for p in prices]
})

sr_finder = SupportResistanceFinder(lookback=10)
key_levels = sr_finder.get_key_levels(demo_df)

print("Key Support Levels:")
for level in key_levels['support'][:3]:
    print(f"  {level['level']:.5f} - {level['touches']} touches ({level['strength']})")

print("\nKey Resistance Levels:")
for level in key_levels['resistance'][:3]:
    print(f"  {level['level']:.5f} - {level['touches']} touches ({level['strength']})")

Trend Line Detection

Trend lines connect swing highs (downtrend) or swing lows (uptrend) to identify trend direction and potential breakout points.

class TrendLineDetector:
    """Automatically detect and validate trend lines."""
    
    def __init__(self, min_touches: int = 3, tolerance: float = 0.001):
        self.min_touches = min_touches
        self.tolerance = tolerance
    
    def find_swing_points(self, df: pd.DataFrame, lookback: int = 5) -> Dict[str, pd.DataFrame]:
        """Identify swing highs and lows."""
        swing_highs = []
        swing_lows = []
        
        for i in range(lookback, len(df) - lookback):
            if df['high'].iloc[i] == df['high'].iloc[i-lookback:i+lookback+1].max():
                swing_highs.append({'index': i, 'price': df['high'].iloc[i]})
            
            if df['low'].iloc[i] == df['low'].iloc[i-lookback:i+lookback+1].min():
                swing_lows.append({'index': i, 'price': df['low'].iloc[i]})
        
        return {
            'highs': pd.DataFrame(swing_highs),
            'lows': pd.DataFrame(swing_lows)
        }
    
    def fit_trend_line(self, points: pd.DataFrame) -> Optional[Dict]:
        """Fit a trend line through points using linear regression."""
        if len(points) < 2:
            return None
        
        x = points['index'].values
        y = points['price'].values
        
        slope = np.polyfit(x, y, 1)[0]
        intercept = np.mean(y) - slope * np.mean(x)
        
        y_pred = slope * x + intercept
        ss_res = np.sum((y - y_pred) ** 2)
        ss_tot = np.sum((y - np.mean(y)) ** 2)
        r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
        
        return {
            'slope': slope,
            'intercept': intercept,
            'r_squared': r_squared,
            'points_used': len(points),
            'direction': 'up' if slope > 0 else 'down'
        }
    
    def detect_trend_lines(self, df: pd.DataFrame) -> Dict[str, List[Dict]]:
        """Detect both uptrend and downtrend lines."""
        swings = self.find_swing_points(df)
        trend_lines = {'uptrend': [], 'downtrend': []}
        
        if len(swings['lows']) >= self.min_touches:
            recent_lows = swings['lows'].tail(10)
            trendline = self.fit_trend_line(recent_lows)
            if trendline and trendline['slope'] > 0 and trendline['r_squared'] > 0.7:
                trend_lines['uptrend'].append(trendline)
        
        if len(swings['highs']) >= self.min_touches:
            recent_highs = swings['highs'].tail(10)
            trendline = self.fit_trend_line(recent_highs)
            if trendline and trendline['slope'] < 0 and trendline['r_squared'] > 0.7:
                trend_lines['downtrend'].append(trendline)
        
        return trend_lines


# Demo
detector = TrendLineDetector()
trend_lines = detector.detect_trend_lines(demo_df)

print("Detected Trend Lines:")
for direction, lines in trend_lines.items():
    for line in lines:
        print(f"  {direction.upper()}: slope={line['slope']:.6f}, R²={line['r_squared']:.3f}")

Chart Pattern Recognition

Common patterns include head & shoulders, double tops/bottoms, triangles, and flags.

class ChartPatternRecognizer:
    """Recognize common chart patterns in price data."""
    
    def __init__(self, tolerance: float = 0.005):
        self.tolerance = tolerance
    
    def find_double_top(self, df: pd.DataFrame, lookback: int = 50) -> Optional[Dict]:
        """Detect double top pattern (bearish reversal)."""
        recent = df.tail(lookback)
        highs = recent['high'].values
        
        peak_indices = []
        for i in range(5, len(highs) - 5):
            if highs[i] == max(highs[i-5:i+6]):
                peak_indices.append(i)
        
        if len(peak_indices) >= 2:
            peak1_idx, peak2_idx = peak_indices[-2], peak_indices[-1]
            peak1, peak2 = highs[peak1_idx], highs[peak2_idx]
            
            if abs(peak1 - peak2) / peak1 < self.tolerance:
                neckline = min(recent['low'].iloc[peak1_idx:peak2_idx])
                return {
                    'pattern': 'double_top',
                    'signal': 'bearish',
                    'peak1': peak1,
                    'peak2': peak2,
                    'neckline': neckline,
                    'target': neckline - (peak1 - neckline)
                }
        return None
    
    def find_double_bottom(self, df: pd.DataFrame, lookback: int = 50) -> Optional[Dict]:
        """Detect double bottom pattern (bullish reversal)."""
        recent = df.tail(lookback)
        lows = recent['low'].values
        
        trough_indices = []
        for i in range(5, len(lows) - 5):
            if lows[i] == min(lows[i-5:i+6]):
                trough_indices.append(i)
        
        if len(trough_indices) >= 2:
            trough1_idx, trough2_idx = trough_indices[-2], trough_indices[-1]
            trough1, trough2 = lows[trough1_idx], lows[trough2_idx]
            
            if abs(trough1 - trough2) / trough1 < self.tolerance:
                neckline = max(recent['high'].iloc[trough1_idx:trough2_idx])
                return {
                    'pattern': 'double_bottom',
                    'signal': 'bullish',
                    'trough1': trough1,
                    'trough2': trough2,
                    'neckline': neckline,
                    'target': neckline + (neckline - trough1)
                }
        return None
    
    def find_triangle(self, df: pd.DataFrame, lookback: int = 40) -> Optional[Dict]:
        """Detect triangle patterns."""
        recent = df.tail(lookback)
        x = np.arange(len(recent))
        high_slope = np.polyfit(x, recent['high'].values, 1)[0]
        low_slope = np.polyfit(x, recent['low'].values, 1)[0]
        
        if high_slope < -0.0001 and low_slope > 0.0001:
            pattern_type = 'symmetrical'
            signal = 'neutral'
        elif abs(high_slope) < 0.0001 and low_slope > 0.0001:
            pattern_type = 'ascending'
            signal = 'bullish'
        elif high_slope < -0.0001 and abs(low_slope) < 0.0001:
            pattern_type = 'descending'
            signal = 'bearish'
        else:
            return None
        
        return {
            'pattern': f'{pattern_type}_triangle',
            'signal': signal,
            'high_slope': high_slope,
            'low_slope': low_slope
        }
    
    def scan_patterns(self, df: pd.DataFrame) -> List[Dict]:
        """Scan for all patterns."""
        patterns = []
        if (dt := self.find_double_top(df)):
            patterns.append(dt)
        if (db := self.find_double_bottom(df)):
            patterns.append(db)
        if (tri := self.find_triangle(df)):
            patterns.append(tri)
        return patterns


# Demo
pattern_recognizer = ChartPatternRecognizer()
patterns = pattern_recognizer.scan_patterns(demo_df)

print("Detected Patterns:")
for p in patterns:
    print(f"  {p['pattern']}: {p['signal']} signal")

5.2 Forex-Specific Indicators

Forex markets have unique indicators not commonly used in equity markets.

Currency Strength Meter

Measures the relative strength of individual currencies across multiple pairs.

class CurrencyStrengthMeter:
    """Calculate relative strength of currencies."""
    
    MAJOR_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'CHF', 'NZD']
    
    PAIR_MAPPING = {
        'EURUSD': ('EUR', 'USD'),
        'GBPUSD': ('GBP', 'USD'),
        'USDJPY': ('USD', 'JPY'),
        'AUDUSD': ('AUD', 'USD'),
        'USDCAD': ('USD', 'CAD'),
        'USDCHF': ('USD', 'CHF'),
        'NZDUSD': ('NZD', 'USD'),
        'EURGBP': ('EUR', 'GBP'),
        'EURJPY': ('EUR', 'JPY'),
        'GBPJPY': ('GBP', 'JPY'),
    }
    
    def __init__(self, period: int = 14):
        self.period = period
    
    def calculate_pair_change(self, prices: pd.Series) -> float:
        """Calculate percentage change over period."""
        if len(prices) < self.period:
            return 0.0
        return (prices.iloc[-1] / prices.iloc[-self.period] - 1) * 100
    
    def calculate_strength(self, pair_data: Dict[str, pd.Series]) -> Dict[str, float]:
        """Calculate strength score for each currency."""
        strength = {currency: 0.0 for currency in self.MAJOR_CURRENCIES}
        count = {currency: 0 for currency in self.MAJOR_CURRENCIES}
        
        for pair, prices in pair_data.items():
            if pair not in self.PAIR_MAPPING:
                continue
            
            base, quote = self.PAIR_MAPPING[pair]
            change = self.calculate_pair_change(prices)
            
            strength[base] += change
            count[base] += 1
            strength[quote] -= change
            count[quote] += 1
        
        for currency in self.MAJOR_CURRENCIES:
            if count[currency] > 0:
                strength[currency] /= count[currency]
        
        return strength
    
    def get_rankings(self, strength: Dict[str, float]) -> List[Tuple[str, float]]:
        """Rank currencies from strongest to weakest."""
        return sorted(strength.items(), key=lambda x: x[1], reverse=True)
    
    def get_best_pairs(self, strength: Dict[str, float], top_n: int = 3) -> List[Dict]:
        """Find best pairs to trade (strong vs weak currencies)."""
        rankings = self.get_rankings(strength)
        strongest = [r[0] for r in rankings[:top_n]]
        weakest = [r[0] for r in rankings[-top_n:]]
        
        opportunities = []
        for strong in strongest:
            for weak in weakest:
                for pair, (base, quote) in self.PAIR_MAPPING.items():
                    if base == strong and quote == weak:
                        opportunities.append({
                            'pair': pair,
                            'direction': 'LONG',
                            'strength_diff': strength[strong] - strength[weak]
                        })
                    elif base == weak and quote == strong:
                        opportunities.append({
                            'pair': pair,
                            'direction': 'SHORT',
                            'strength_diff': strength[strong] - strength[weak]
                        })
        
        return sorted(opportunities, key=lambda x: x['strength_diff'], reverse=True)


# Demo
np.random.seed(42)
pair_data = {}
for pair in CurrencyStrengthMeter.PAIR_MAPPING.keys():
    base_price = 1.0 if 'JPY' not in pair else 110.0
    prices = [base_price]
    for _ in range(50):
        prices.append(prices[-1] * (1 + np.random.normal(0, 0.005)))
    pair_data[pair] = pd.Series(prices)

csm = CurrencyStrengthMeter(period=14)
strength = csm.calculate_strength(pair_data)
rankings = csm.get_rankings(strength)

print("Currency Strength Rankings:")
for currency, score in rankings:
    bar = '+' * int(abs(score) * 10) if score > 0 else '-' * int(abs(score) * 10)
    print(f"  {currency}: {score:+.3f}% {bar}")

print("\nBest Trading Opportunities:")
for opp in csm.get_best_pairs(strength)[:3]:
    print(f"  {opp['pair']} {opp['direction']}: strength diff = {opp['strength_diff']:.3f}%")

Pivot Points

Pivot points are significant levels calculated from prior period's high, low, and close. Very popular in forex day trading.

class PivotPointCalculator:
    """Calculate various pivot point types."""
    
    def standard_pivots(self, high: float, low: float, close: float) -> Dict[str, float]:
        """Calculate standard (floor) pivot points."""
        pp = (high + low + close) / 3
        return {
            'R3': high + 2 * (pp - low),
            'R2': pp + (high - low),
            'R1': 2 * pp - low,
            'PP': pp,
            'S1': 2 * pp - high,
            'S2': pp - (high - low),
            'S3': low - 2 * (high - pp)
        }
    
    def fibonacci_pivots(self, high: float, low: float, close: float) -> Dict[str, float]:
        """Calculate Fibonacci pivot points."""
        pp = (high + low + close) / 3
        range_hl = high - low
        return {
            'R3': pp + range_hl * 1.000,
            'R2': pp + range_hl * 0.618,
            'R1': pp + range_hl * 0.382,
            'PP': pp,
            'S1': pp - range_hl * 0.382,
            'S2': pp - range_hl * 0.618,
            'S3': pp - range_hl * 1.000
        }
    
    def camarilla_pivots(self, high: float, low: float, close: float) -> Dict[str, float]:
        """Calculate Camarilla pivot points."""
        range_hl = high - low
        return {
            'R4': close + range_hl * 1.1 / 2,
            'R3': close + range_hl * 1.1 / 4,
            'R2': close + range_hl * 1.1 / 6,
            'R1': close + range_hl * 1.1 / 12,
            'PP': (high + low + close) / 3,
            'S1': close - range_hl * 1.1 / 12,
            'S2': close - range_hl * 1.1 / 6,
            'S3': close - range_hl * 1.1 / 4,
            'S4': close - range_hl * 1.1 / 2
        }


# Demo
pivot_calc = PivotPointCalculator()

yesterday_high = 1.0920
yesterday_low = 1.0845
yesterday_close = 1.0878

print("Standard Pivot Points:")
std_pivots = pivot_calc.standard_pivots(yesterday_high, yesterday_low, yesterday_close)
for level, price in std_pivots.items():
    print(f"  {level}: {price:.5f}")

print("\nFibonacci Pivot Points:")
fib_pivots = pivot_calc.fibonacci_pivots(yesterday_high, yesterday_low, yesterday_close)
for level, price in fib_pivots.items():
    print(f"  {level}: {price:.5f}")

Fibonacci Retracements and Extensions

Fibonacci levels are heavily used in forex for identifying potential reversal zones and profit targets.

class FibonacciAnalyzer:
    """Calculate Fibonacci retracements and extensions."""
    
    RETRACEMENT_LEVELS = [0.236, 0.382, 0.5, 0.618, 0.786]
    EXTENSION_LEVELS = [1.0, 1.272, 1.618, 2.0, 2.618]
    
    def calculate_retracements(self, swing_high: float, swing_low: float,
                               trend: str = 'up') -> Dict[str, float]:
        """Calculate Fibonacci retracement levels."""
        diff = swing_high - swing_low
        levels = {}
        
        for fib in self.RETRACEMENT_LEVELS:
            if trend == 'up':
                levels[f'{fib:.1%}'] = swing_high - diff * fib
            else:
                levels[f'{fib:.1%}'] = swing_low + diff * fib
        
        levels['0.0%'] = swing_high if trend == 'up' else swing_low
        levels['100.0%'] = swing_low if trend == 'up' else swing_high
        
        return levels
    
    def calculate_extensions(self, swing_high: float, swing_low: float,
                            retracement_point: float, trend: str = 'up') -> Dict[str, float]:
        """Calculate Fibonacci extension levels for profit targets."""
        diff = swing_high - swing_low
        levels = {}
        
        for fib in self.EXTENSION_LEVELS:
            if trend == 'up':
                levels[f'{fib:.1%}'] = retracement_point + diff * fib
            else:
                levels[f'{fib:.1%}'] = retracement_point - diff * fib
        
        return levels


# Demo
fib_analyzer = FibonacciAnalyzer()

swing_low = 1.0750
swing_high = 1.0950

retracements = fib_analyzer.calculate_retracements(swing_high, swing_low, 'up')
print("Fibonacci Retracements (Uptrend):")
for level, price in sorted(retracements.items(), key=lambda x: x[1], reverse=True):
    print(f"  {level}: {price:.5f}")

bounce_point = retracements['61.8%']
extensions = fib_analyzer.calculate_extensions(swing_high, swing_low, bounce_point, 'up')
print(f"\nFibonacci Extensions (from bounce at {bounce_point:.5f}):")
for level, price in sorted(extensions.items(), key=lambda x: x[1]):
    print(f"  {level}: {price:.5f}")

5.3 Multi-Timeframe Analysis

Multi-timeframe analysis (MTF) uses multiple chart timeframes to identify high-probability trades by aligning trend direction across timeframes.

Top-Down Approach

Start with higher timeframes for trend direction, then drill down for entries.

class MultiTimeframeAnalyzer:
    """Analyze price action across multiple timeframes."""
    
    TIMEFRAME_HIERARCHY = ['W', 'D', '4h', '1h', '15min']
    
    def __init__(self):
        self.data = {}
    
    def calculate_trend(self, df: pd.DataFrame, ma_period: int = 20) -> str:
        """Determine trend using moving average."""
        if len(df) < ma_period:
            return 'unknown'
        
        ma = df['close'].rolling(ma_period).mean()
        current_price = df['close'].iloc[-1]
        current_ma = ma.iloc[-1]
        ma_slope = ma.iloc[-1] - ma.iloc[-5] if len(ma) >= 5 else 0
        
        if current_price > current_ma and ma_slope > 0:
            return 'bullish'
        elif current_price < current_ma and ma_slope < 0:
            return 'bearish'
        return 'neutral'
    
    def analyze_timeframes(self, base_df: pd.DataFrame) -> Dict[str, Dict]:
        """Analyze trend across multiple timeframes."""
        results = {}
        
        if 'timestamp' in base_df.columns:
            df = base_df.set_index('timestamp')
        else:
            df = base_df.copy()
        
        timeframes = {'4h': '4h', 'D': 'D'}
        
        for tf_name, resample_rule in timeframes.items():
            try:
                tf_data = df.resample(resample_rule).agg({
                    'open': 'first',
                    'high': 'max',
                    'low': 'min',
                    'close': 'last'
                }).dropna().reset_index()
                
                trend = self.calculate_trend(tf_data)
                results[tf_name] = {
                    'trend': trend,
                    'last_close': tf_data['close'].iloc[-1] if len(tf_data) > 0 else None
                }
            except:
                results[tf_name] = {'trend': 'unknown', 'last_close': None}
        
        return results
    
    def get_alignment_score(self, analysis: Dict[str, Dict]) -> Dict:
        """Calculate how aligned the timeframes are."""
        trends = [a['trend'] for a in analysis.values() if a['trend'] != 'unknown']
        
        if not trends:
            return {'score': 0, 'direction': 'none', 'aligned': False}
        
        bullish_count = trends.count('bullish')
        bearish_count = trends.count('bearish')
        total = len(trends)
        
        if bullish_count == total:
            return {'score': 100, 'direction': 'bullish', 'aligned': True}
        elif bearish_count == total:
            return {'score': 100, 'direction': 'bearish', 'aligned': True}
        elif bullish_count > bearish_count:
            return {'score': (bullish_count / total) * 100, 'direction': 'bullish', 'aligned': False}
        return {'score': (bearish_count / total) * 100, 'direction': 'bearish', 'aligned': False}


# Demo
mtf = MultiTimeframeAnalyzer()
analysis = mtf.analyze_timeframes(demo_df)

print("Multi-Timeframe Analysis:")
for tf, data in analysis.items():
    print(f"  {tf}: {data['trend']}")

alignment = mtf.get_alignment_score(analysis)
print(f"\nAlignment: {alignment['score']:.0f}% {alignment['direction']}")
print(f"Fully Aligned: {alignment['aligned']}")

5.4 Price Action Trading

Price action trading focuses on raw price movements without indicators, using candlestick patterns and key levels.

class CandlestickPatterns:
    """Identify key candlestick patterns."""
    
    def __init__(self, body_threshold: float = 0.3):
        self.body_threshold = body_threshold
    
    def get_candle_properties(self, row: pd.Series) -> Dict:
        """Extract candle properties."""
        open_p, high, low, close = row['open'], row['high'], row['low'], row['close']
        
        range_size = high - low
        body_size = abs(close - open_p)
        upper_wick = high - max(open_p, close)
        lower_wick = min(open_p, close) - low
        
        return {
            'bullish': close > open_p,
            'body_size': body_size,
            'range': range_size,
            'body_pct': body_size / range_size if range_size > 0 else 0,
            'upper_wick_pct': upper_wick / range_size if range_size > 0 else 0,
            'lower_wick_pct': lower_wick / range_size if range_size > 0 else 0
        }
    
    def is_doji(self, props: Dict) -> bool:
        """Check for doji (indecision)."""
        return props['body_pct'] < 0.1
    
    def is_hammer(self, props: Dict) -> bool:
        """Check for hammer/hanging man."""
        return (props['lower_wick_pct'] > 0.6 and
                props['upper_wick_pct'] < 0.1 and
                props['body_pct'] < 0.3)
    
    def is_shooting_star(self, props: Dict) -> bool:
        """Check for shooting star/inverted hammer."""
        return (props['upper_wick_pct'] > 0.6 and
                props['lower_wick_pct'] < 0.1 and
                props['body_pct'] < 0.3)
    
    def scan_patterns(self, df: pd.DataFrame, lookback: int = 5) -> List[Dict]:
        """Scan for patterns in recent candles."""
        patterns = []
        
        for i in range(-lookback, 0):
            row = df.iloc[i]
            props = self.get_candle_properties(row)
            
            if self.is_doji(props):
                patterns.append({'index': i, 'pattern': 'doji', 'signal': 'reversal_warning'})
            elif self.is_hammer(props):
                signal = 'bullish' if props['bullish'] else 'potential_bullish'
                patterns.append({'index': i, 'pattern': 'hammer', 'signal': signal})
            elif self.is_shooting_star(props):
                signal = 'bearish' if not props['bullish'] else 'potential_bearish'
                patterns.append({'index': i, 'pattern': 'shooting_star', 'signal': signal})
        
        return patterns


# Demo
candle_patterns = CandlestickPatterns()
patterns = candle_patterns.scan_patterns(demo_df)

print("Recent Candlestick Patterns:")
for p in patterns[-5:]:
    print(f"  Bar {p['index']}: {p['pattern']} ({p['signal']})")

Exercises

Exercise 1: Support/Resistance Level Finder (Guided)

Complete the EnhancedSRFinder class that identifies support and resistance levels and determines if they're currently being tested.

class EnhancedSRFinder:
    """Enhanced support/resistance finder with level testing detection."""
    
    def __init__(self, lookback: int = 20, proximity_pct: float = 0.002):
        self.lookback = lookback
        self.proximity_pct = proximity_pct
    
    def identify_levels(self, df: pd.DataFrame) -> Dict[str, List[float]]:
        """Identify key S/R levels from swing points."""
        resistance_levels = []
        support_levels = []
        
        for i in range(self.lookback, len(df) - self.lookback):
            window_high = df['high'].iloc[i-self.lookback:i+self.lookback+1]
            if df['high'].iloc[i] == ______():
                resistance_levels.append(df['high'].iloc[i])
            
            window_low = df['low'].iloc[i-self.lookback:i+self.lookback+1]
            if df['low'].iloc[i] == ______():
                support_levels.append(df['low'].iloc[i])
        
        return {'resistance': resistance_levels, 'support': support_levels}
    
    def is_testing_level(self, current_price: float, level: float) -> bool:
        """Check if price is testing (near) a level."""
        distance_pct = ______(current_price - level) / level
        return distance_pct <= self.proximity_pct
    
    def get_nearest_levels(self, df: pd.DataFrame, current_price: float) -> Dict[str, Dict]:
        """Get nearest support and resistance with testing status."""
        levels = self.identify_levels(df)
        
        resistance_above = [r for r in levels['resistance'] if r > current_price]
        nearest_resistance = ______(resistance_above) if resistance_above else None
        
        support_below = [s for s in levels['support'] if s < current_price]
        nearest_support = ______(support_below) if support_below else None
        
        result = {}
        if nearest_resistance:
            result['resistance'] = {
                'level': nearest_resistance,
                'is_testing': self.is_testing_level(current_price, nearest_resistance)
            }
        if nearest_support:
            result['support'] = {
                'level': nearest_support,
                'is_testing': self.is_testing_level(current_price, nearest_support)
            }
        
        return result


# Test
sr_finder = EnhancedSRFinder(lookback=10)
current_price = demo_df['close'].iloc[-1]
nearest = sr_finder.get_nearest_levels(demo_df, current_price)

print(f"Current Price: {current_price:.5f}")
if 'resistance' in nearest:
    r = nearest['resistance']
    print(f"Nearest Resistance: {r['level']:.5f} (Testing: {r['is_testing']})")
if 'support' in nearest:
    s = nearest['support']
    print(f"Nearest Support: {s['level']:.5f} (Testing: {s['is_testing']})")
Solution 1
class EnhancedSRFinder:
    def __init__(self, lookback: int = 20, proximity_pct: float = 0.002):
        self.lookback = lookback
        self.proximity_pct = proximity_pct

    def identify_levels(self, df: pd.DataFrame) -> Dict[str, List[float]]:
        resistance_levels = []
        support_levels = []

        for i in range(self.lookback, len(df) - self.lookback):
            window_high = df['high'].iloc[i-self.lookback:i+self.lookback+1]
            if df['high'].iloc[i] == window_high.max():
                resistance_levels.append(df['high'].iloc[i])

            window_low = df['low'].iloc[i-self.lookback:i+self.lookback+1]
            if df['low'].iloc[i] == window_low.min():
                support_levels.append(df['low'].iloc[i])

        return {'resistance': resistance_levels, 'support': support_levels}

    def is_testing_level(self, current_price: float, level: float) -> bool:
        distance_pct = abs(current_price - level) / level
        return distance_pct <= self.proximity_pct

    def get_nearest_levels(self, df: pd.DataFrame, current_price: float) -> Dict[str, Dict]:
        levels = self.identify_levels(df)

        resistance_above = [r for r in levels['resistance'] if r > current_price]
        nearest_resistance = min(resistance_above) if resistance_above else None

        support_below = [s for s in levels['support'] if s < current_price]
        nearest_support = max(support_below) if support_below else None

        result = {}
        if nearest_resistance:
            result['resistance'] = {
                'level': nearest_resistance,
                'is_testing': self.is_testing_level(current_price, nearest_resistance)
            }
        if nearest_support:
            result['support'] = {
                'level': nearest_support,
                'is_testing': self.is_testing_level(current_price, nearest_support)
            }

        return result

Exercise 2: Currency Strength Calculator (Guided)

Complete the QuickStrengthMeter that calculates currency strength using a simplified method.

class QuickStrengthMeter:
    """Simplified currency strength calculator."""
    
    USD_PAIRS = {
        'EURUSD': 'quote',
        'GBPUSD': 'quote',
        'USDJPY': 'base',
        'USDCHF': 'base',
        'AUDUSD': 'quote',
        'USDCAD': 'base'
    }
    
    def __init__(self, period: int = 10):
        self.period = period
    
    def calculate_usd_strength(self, pair_changes: Dict[str, float]) -> float:
        """Calculate USD strength from pair percentage changes."""
        usd_strength = 0.0
        pair_count = 0
        
        for pair, position in self.USD_PAIRS.items():
            if pair not in pair_changes:
                continue
            
            change = pair_changes[pair]
            
            if position == 'base':
                usd_strength ______ change
            else:
                usd_strength ______ change
            
            pair_count += 1
        
        return usd_strength / pair_count if pair_count > 0 else 0.0
    
    def get_pair_changes(self, pair_data: Dict[str, pd.Series]) -> Dict[str, float]:
        """Calculate percentage change for each pair."""
        changes = {}
        
        for pair, prices in pair_data.items():
            if len(prices) >= self.period:
                start_price = prices.iloc[-self.period]
                end_price = prices.iloc[-1]
                changes[pair] = ((end_price - start_price) / start_price) * ______
        
        return changes
    
    def analyze(self, pair_data: Dict[str, pd.Series]) -> Dict:
        """Full analysis of USD strength."""
        changes = self.get_pair_changes(pair_data)
        usd_strength = self.calculate_usd_strength(changes)
        
        return {
            'usd_strength': usd_strength,
            'interpretation': 'bullish' if usd_strength > 0 else 'bearish',
            'pair_changes': changes
        }


# Test
test_pairs = {}
np.random.seed(123)
for pair in QuickStrengthMeter.USD_PAIRS.keys():
    base = 1.10 if 'JPY' not in pair else 150.0
    test_pairs[pair] = pd.Series([base * (1 + np.random.normal(0.001, 0.005)) for _ in range(20)])

meter = QuickStrengthMeter()
result = meter.analyze(test_pairs)

print(f"USD Strength: {result['usd_strength']:.3f}%")
print(f"Interpretation: USD is {result['interpretation']}")
Solution 2
class QuickStrengthMeter:
    USD_PAIRS = {
        'EURUSD': 'quote',
        'GBPUSD': 'quote',
        'USDJPY': 'base',
        'USDCHF': 'base',
        'AUDUSD': 'quote',
        'USDCAD': 'base'
    }

    def __init__(self, period: int = 10):
        self.period = period

    def calculate_usd_strength(self, pair_changes: Dict[str, float]) -> float:
        usd_strength = 0.0
        pair_count = 0

        for pair, position in self.USD_PAIRS.items():
            if pair not in pair_changes:
                continue

            change = pair_changes[pair]

            if position == 'base':
                usd_strength += change
            else:
                usd_strength -= change

            pair_count += 1

        return usd_strength / pair_count if pair_count > 0 else 0.0

    def get_pair_changes(self, pair_data: Dict[str, pd.Series]) -> Dict[str, float]:
        changes = {}

        for pair, prices in pair_data.items():
            if len(prices) >= self.period:
                start_price = prices.iloc[-self.period]
                end_price = prices.iloc[-1]
                changes[pair] = ((end_price - start_price) / start_price) * 100

        return changes

    def analyze(self, pair_data: Dict[str, pd.Series]) -> Dict:
        changes = self.get_pair_changes(pair_data)
        usd_strength = self.calculate_usd_strength(changes)

        return {
            'usd_strength': usd_strength,
            'interpretation': 'bullish' if usd_strength > 0 else 'bearish',
            'pair_changes': changes
        }

Exercise 3: Pivot Point Trader (Guided)

Complete the PivotTrader class that generates signals based on pivot point levels.

class PivotTrader:
    """Generate trading signals from pivot points."""
    
    def __init__(self, bounce_threshold: float = 0.0003):
        self.bounce_threshold = bounce_threshold
        self.pivot_calc = PivotPointCalculator()
    
    def get_position_vs_pivot(self, price: float, pivots: Dict[str, float]) -> str:
        """Determine price position relative to pivot."""
        pp = pivots['PP']
        
        if price > pivots.get('R2', pp * 1.01):
            return 'above_r2'
        elif price > pivots.get('R1', pp * 1.005):
            return 'above_r1'
        elif price > pp:
            return 'above_pp'
        elif price > pivots.get('S1', pp * 0.995):
            return 'below_pp'
        elif price > pivots.get('S2', pp * 0.99):
            return 'below_s1'
        return 'below_s2'
    
    def check_bounce(self, current: float, prev: float, level: float) -> Optional[str]:
        """Check if price bounced off a level."""
        if abs(prev - level) / level < self.bounce_threshold:
            if current > prev:
                return 'bullish_bounce'
            elif current < prev:
                return 'bearish_bounce'
        return None
    
    def generate_signal(self, current_price: float, prev_price: float,
                       yesterday_high: float, yesterday_low: float,
                       yesterday_close: float) -> Dict:
        """Generate trading signal based on pivot levels."""
        pivots = self.pivot_calc.______(yesterday_high, yesterday_low, yesterday_close)
        position = self.get_position_vs_pivot(current_price, pivots)
        
        signal = {
            'position': position,
            'pivots': pivots,
            'action': 'HOLD',
            'reason': ''
        }
        
        for level_name in ['PP', 'S1', 'S2', 'R1', 'R2']:
            level = pivots.get(level_name)
            if level:
                bounce = self.check_bounce(current_price, prev_price, level)
                if bounce == 'bullish_bounce' and level_name in ['PP', 'S1', 'S2']:
                    signal['action'] = '______'
                    signal['reason'] = f'Bullish bounce at {level_name}'
                    break
                elif bounce == 'bearish_bounce' and level_name in ['PP', 'R1', 'R2']:
                    signal['action'] = '______'
                    signal['reason'] = f'Bearish bounce at {level_name}'
                    break
        
        return signal


# Test
trader = PivotTrader()

y_high, y_low, y_close = 1.0920, 1.0845, 1.0878
pivots = PivotPointCalculator().standard_pivots(y_high, y_low, y_close)
prev_price = pivots['S1'] + 0.0001
current_price = pivots['S1'] + 0.0010

signal = trader.generate_signal(current_price, prev_price, y_high, y_low, y_close)

print(f"Current Price: {current_price:.5f}")
print(f"Position: {signal['position']}")
print(f"Action: {signal['action']}")
print(f"Reason: {signal['reason']}")
Solution 3
class PivotTrader:
    def __init__(self, bounce_threshold: float = 0.0003):
        self.bounce_threshold = bounce_threshold
        self.pivot_calc = PivotPointCalculator()

    def get_position_vs_pivot(self, price: float, pivots: Dict[str, float]) -> str:
        pp = pivots['PP']

        if price > pivots.get('R2', pp * 1.01):
            return 'above_r2'
        elif price > pivots.get('R1', pp * 1.005):
            return 'above_r1'
        elif price > pp:
            return 'above_pp'
        elif price > pivots.get('S1', pp * 0.995):
            return 'below_pp'
        elif price > pivots.get('S2', pp * 0.99):
            return 'below_s1'
        return 'below_s2'

    def check_bounce(self, current: float, prev: float, level: float) -> Optional[str]:
        if abs(prev - level) / level < self.bounce_threshold:
            if current > prev:
                return 'bullish_bounce'
            elif current < prev:
                return 'bearish_bounce'
        return None

    def generate_signal(self, current_price: float, prev_price: float,
                       yesterday_high: float, yesterday_low: float,
                       yesterday_close: float) -> Dict:
        pivots = self.pivot_calc.standard_pivots(yesterday_high, yesterday_low, yesterday_close)
        position = self.get_position_vs_pivot(current_price, pivots)

        signal = {
            'position': position,
            'pivots': pivots,
            'action': 'HOLD',
            'reason': ''
        }

        for level_name in ['PP', 'S1', 'S2', 'R1', 'R2']:
            level = pivots.get(level_name)
            if level:
                bounce = self.check_bounce(current_price, prev_price, level)
                if bounce == 'bullish_bounce' and level_name in ['PP', 'S1', 'S2']:
                    signal['action'] = 'BUY'
                    signal['reason'] = f'Bullish bounce at {level_name}'
                    break
                elif bounce == 'bearish_bounce' and level_name in ['PP', 'R1', 'R2']:
                    signal['action'] = 'SELL'
                    signal['reason'] = f'Bearish bounce at {level_name}'
                    break

        return signal

Exercise 4: Multi-Timeframe Trend Analyzer (Open-ended)

Build a comprehensive MTF analyzer that: - Analyzes 3+ timeframes (e.g., Daily, 4H, 1H) - Calculates trend direction for each using your choice of method - Returns an alignment score and trading bias - Identifies the best timeframe for entry

# Exercise 4: Multi-Timeframe Trend Analyzer (Open-ended)
#
# Requirements:
# 1. Create class ComprehensiveMTFAnalyzer
# 2. Support at least 3 timeframes
# 3. Calculate trend using MA crossover or ADX
# 4. Return alignment score (0-100)
# 5. Provide trading recommendation
#
# Your implementation:
Solution 4
class ComprehensiveMTFAnalyzer:
    """Multi-timeframe trend analyzer with alignment scoring."""

    def __init__(self, timeframes: List[str] = None):
        self.timeframes = timeframes or ['D', '4h', '1h']
        self.tf_weights = {'D': 3, '4h': 2, '1h': 1}

    def calculate_trend(self, df: pd.DataFrame, fast: int = 10, slow: int = 20) -> Dict:
        if len(df) < slow:
            return {'direction': 'unknown', 'strength': 0}

        ma_fast = df['close'].rolling(fast).mean()
        ma_slow = df['close'].rolling(slow).mean()
        diff = (ma_fast.iloc[-1] - ma_slow.iloc[-1]) / ma_slow.iloc[-1]

        if diff > 0.002:
            return {'direction': 'bullish', 'strength': min(abs(diff) * 100, 100)}
        elif diff < -0.002:
            return {'direction': 'bearish', 'strength': min(abs(diff) * 100, 100)}
        return {'direction': 'neutral', 'strength': 0}

    def analyze(self, df: pd.DataFrame) -> Dict:
        results = {}
        if 'timestamp' in df.columns:
            df = df.set_index('timestamp')

        for tf in self.timeframes:
            tf_data = df.resample(tf).agg({
                'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'
            }).dropna()
            results[tf] = self.calculate_trend(tf_data)

        bullish_score = sum(
            self.tf_weights.get(tf, 1) * r['strength']
            for tf, r in results.items() if r['direction'] == 'bullish'
        )
        bearish_score = sum(
            self.tf_weights.get(tf, 1) * r['strength']
            for tf, r in results.items() if r['direction'] == 'bearish'
        )

        total_weight = sum(self.tf_weights.get(tf, 1) for tf in self.timeframes)

        if bullish_score > bearish_score:
            bias = 'bullish'
            alignment = bullish_score / (total_weight * 100) * 100
        elif bearish_score > bullish_score:
            bias = 'bearish'
            alignment = bearish_score / (total_weight * 100) * 100
        else:
            bias = 'neutral'
            alignment = 0

        return {
            'timeframes': results,
            'bias': bias,
            'alignment_score': alignment,
            'recommendation': 'TRADE' if alignment > 60 else 'WAIT'
        }

Exercise 5: Candlestick Pattern Scanner (Open-ended)

Build an advanced candlestick scanner that: - Identifies at least 5 different patterns - Assigns probability scores to each pattern - Considers the context (trend, key levels) - Returns actionable trading signals

# Exercise 5: Candlestick Pattern Scanner (Open-ended)
#
# Requirements:
# 1. Create class AdvancedCandleScanner
# 2. Detect: doji, hammer, engulfing, morning/evening star, three soldiers/crows
# 3. Score pattern quality (0-100)
# 4. Consider if pattern is at key S/R level
# 5. Generate trade signal with entry/stop/target
#
# Your implementation:
Solution 5
class AdvancedCandleScanner:
    """Advanced candlestick pattern scanner with context awareness."""

    def __init__(self):
        self.sr_finder = SupportResistanceFinder(lookback=10)

    def get_candle_type(self, o: float, h: float, l: float, c: float) -> Dict:
        body = abs(c - o)
        range_size = h - l
        upper_wick = h - max(o, c)
        lower_wick = min(o, c) - l

        return {
            'bullish': c > o,
            'body_pct': body / range_size if range_size > 0 else 0,
            'upper_wick_pct': upper_wick / range_size if range_size > 0 else 0,
            'lower_wick_pct': lower_wick / range_size if range_size > 0 else 0,
            'body': body,
            'range': range_size
        }

    def detect_patterns(self, df: pd.DataFrame) -> List[Dict]:
        patterns = []

        for i in range(-5, 0):
            row = df.iloc[i]
            candle = self.get_candle_type(row['open'], row['high'], row['low'], row['close'])

            if candle['body_pct'] < 0.1:
                patterns.append({'idx': i, 'pattern': 'doji', 'signal': 'neutral', 'score': 60})
            if candle['lower_wick_pct'] > 0.6 and candle['body_pct'] < 0.3:
                patterns.append({'idx': i, 'pattern': 'hammer', 'signal': 'bullish', 'score': 70})
            if candle['upper_wick_pct'] > 0.6 and candle['body_pct'] < 0.3:
                patterns.append({'idx': i, 'pattern': 'shooting_star', 'signal': 'bearish', 'score': 70})

            if i > -len(df) + 1:
                prev = df.iloc[i-1]
                prev_candle = self.get_candle_type(prev['open'], prev['high'], prev['low'], prev['close'])

                if candle['body'] > prev_candle['body'] * 1.3:
                    if candle['bullish'] and not prev_candle['bullish']:
                        patterns.append({'idx': i, 'pattern': 'bullish_engulfing', 'signal': 'bullish', 'score': 80})
                    elif not candle['bullish'] and prev_candle['bullish']:
                        patterns.append({'idx': i, 'pattern': 'bearish_engulfing', 'signal': 'bearish', 'score': 80})

        return patterns

    def generate_signals(self, df: pd.DataFrame) -> List[Dict]:
        patterns = self.detect_patterns(df)
        current_price = df['close'].iloc[-1]
        atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1]
        levels = self.sr_finder.get_key_levels(df)

        signals = []
        for p in patterns:
            if p['idx'] < -2:
                continue

            at_support = any(abs(current_price - s['level']) / s['level'] < 0.003 for s in levels['support'])
            at_resistance = any(abs(current_price - r['level']) / r['level'] < 0.003 for r in levels['resistance'])

            score = p['score']
            if at_support and p['signal'] == 'bullish':
                score += 20
            elif at_resistance and p['signal'] == 'bearish':
                score += 20

            if score >= 70:
                signals.append({
                    'pattern': p['pattern'],
                    'action': 'BUY' if p['signal'] == 'bullish' else 'SELL',
                    'score': min(score, 100),
                    'entry': current_price,
                    'stop': current_price - atr * 1.5 if p['signal'] == 'bullish' else current_price + atr * 1.5,
                    'target': current_price + atr * 2 if p['signal'] == 'bullish' else current_price - atr * 2
                })

        return sorted(signals, key=lambda x: x['score'], reverse=True)

Exercise 6: Complete Technical Analysis Suite (Open-ended)

Build a comprehensive technical analysis class that combines: - Support/resistance levels - Trend analysis - Chart patterns - Candlestick patterns - Multi-timeframe confirmation

Generate a complete market analysis report.

# Exercise 6: Complete Technical Analysis Suite (Open-ended)
#
# Requirements:
# 1. Create class TechnicalAnalysisSuite
# 2. Combine S/R, trend, patterns, candles, MTF
# 3. Generate comprehensive analysis report
# 4. Provide overall bias with confidence
# 5. Output actionable trade setup if conditions align
#
# Your implementation:
Solution 6
class TechnicalAnalysisSuite:
    """Comprehensive technical analysis combining multiple methods."""

    def __init__(self):
        self.sr_finder = SupportResistanceFinder(lookback=10)
        self.pattern_recognizer = ChartPatternRecognizer()
        self.candle_scanner = CandlestickPatterns()
        self.mtf_analyzer = MultiTimeframeAnalyzer()

    def analyze_trend(self, df: pd.DataFrame) -> Dict:
        ma20 = df['close'].rolling(20).mean()
        ma50 = df['close'].rolling(50).mean()
        current = df['close'].iloc[-1]

        if len(ma50.dropna()) < 1:
            return {'direction': 'unknown', 'strength': 0}

        if current > ma20.iloc[-1] > ma50.iloc[-1]:
            return {'direction': 'bullish', 'strength': 80}
        elif current < ma20.iloc[-1] < ma50.iloc[-1]:
            return {'direction': 'bearish', 'strength': 80}
        return {'direction': 'neutral', 'strength': 30}

    def full_analysis(self, df: pd.DataFrame) -> Dict:
        report = {
            'timestamp': pd.Timestamp.now(),
            'current_price': df['close'].iloc[-1],
            'trend': self.analyze_trend(df),
            'key_levels': self.sr_finder.get_key_levels(df),
            'chart_patterns': self.pattern_recognizer.scan_patterns(df),
            'candle_patterns': self.candle_scanner.scan_patterns(df, lookback=3),
            'mtf': self.mtf_analyzer.analyze_timeframes(df)
        }

        bullish_signals = bearish_signals = 0
        if report['trend']['direction'] == 'bullish':
            bullish_signals += 2
        elif report['trend']['direction'] == 'bearish':
            bearish_signals += 2

        for p in report['chart_patterns']:
            if p['signal'] == 'bullish': bullish_signals += 1
            elif p['signal'] == 'bearish': bearish_signals += 1

        for c in report['candle_patterns']:
            if 'bullish' in c.get('signal', ''): bullish_signals += 1
            elif 'bearish' in c.get('signal', ''): bearish_signals += 1

        total = bullish_signals + bearish_signals
        if total > 0:
            if bullish_signals > bearish_signals:
                report['overall_bias'] = 'bullish'
                report['confidence'] = bullish_signals / total * 100
            else:
                report['overall_bias'] = 'bearish'
                report['confidence'] = bearish_signals / total * 100
        else:
            report['overall_bias'] = 'neutral'
            report['confidence'] = 0

        if report['confidence'] > 60:
            atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1]
            entry = report['current_price']

            if report['overall_bias'] == 'bullish':
                report['trade_setup'] = {
                    'action': 'BUY', 'entry': entry,
                    'stop': entry - atr * 1.5, 'target': entry + atr * 2.5
                }
            else:
                report['trade_setup'] = {
                    'action': 'SELL', 'entry': entry,
                    'stop': entry + atr * 1.5, 'target': entry - atr * 2.5
                }
        else:
            report['trade_setup'] = None

        return report

Module Project: Technical Analysis Suite

Build a production-ready technical analysis system that combines all concepts from this module.

class ForexTechnicalAnalyzer:
    """
    Production-ready forex technical analysis system.
    
    Combines: S/R detection, Trend analysis, Chart patterns,
    Candlestick patterns, MTF confirmation, Pivot points, Fibonacci.
    """
    
    def __init__(self, pair: str = 'EURUSD'):
        self.pair = pair
        self.sr_finder = SupportResistanceFinder(lookback=15)
        self.pattern_detector = ChartPatternRecognizer()
        self.candle_scanner = CandlestickPatterns()
        self.mtf_analyzer = MultiTimeframeAnalyzer()
        self.pivot_calc = PivotPointCalculator()
        self.fib_analyzer = FibonacciAnalyzer()
    
    def analyze_structure(self, df: pd.DataFrame) -> Dict:
        """Analyze market structure."""
        levels = self.sr_finder.get_key_levels(df)
        current = df['close'].iloc[-1]
        
        resistance_above = [r for r in levels['resistance'] if r['level'] > current]
        support_below = [s for s in levels['support'] if s['level'] < current]
        
        return {
            'current_price': current,
            'nearest_resistance': min(resistance_above, key=lambda x: x['level']) if resistance_above else None,
            'nearest_support': max(support_below, key=lambda x: x['level']) if support_below else None
        }
    
    def analyze_trend(self, df: pd.DataFrame) -> Dict:
        """Comprehensive trend analysis."""
        ma10 = df['close'].rolling(10).mean()
        ma20 = df['close'].rolling(20).mean()
        ma50 = df['close'].rolling(50).mean()
        current = df['close'].iloc[-1]
        
        if current > ma10.iloc[-1] > ma20.iloc[-1] > ma50.iloc[-1]:
            return {'direction': 'strong_bullish', 'strength': 100}
        elif current > ma20.iloc[-1] > ma50.iloc[-1]:
            return {'direction': 'bullish', 'strength': 75}
        elif current < ma10.iloc[-1] < ma20.iloc[-1] < ma50.iloc[-1]:
            return {'direction': 'strong_bearish', 'strength': 100}
        elif current < ma20.iloc[-1] < ma50.iloc[-1]:
            return {'direction': 'bearish', 'strength': 75}
        return {'direction': 'ranging', 'strength': 30}
    
    def calculate_bias(self, structure: Dict, trend: Dict, patterns: Dict, pivots: Dict) -> Dict:
        """Calculate overall trading bias."""
        score = 0
        factors = []
        
        if 'bullish' in trend['direction']:
            score += 3 * (trend['strength'] / 100)
            factors.append(f"Trend: {trend['direction']}")
        elif 'bearish' in trend['direction']:
            score -= 3 * (trend['strength'] / 100)
            factors.append(f"Trend: {trend['direction']}")
        
        for p in patterns.get('chart_patterns', []):
            if p['signal'] == 'bullish':
                score += 2
                factors.append(f"Chart: {p['pattern']}")
            elif p['signal'] == 'bearish':
                score -= 2
                factors.append(f"Chart: {p['pattern']}")
        
        if pivots:
            current = structure['current_price']
            pp = pivots.get('PP', current)
            if current > pp:
                score += 1
                factors.append("Above pivot")
            else:
                score -= 1
                factors.append("Below pivot")
        
        if score > 2:
            bias = 'bullish'
            confidence = min(score * 15, 100)
        elif score < -2:
            bias = 'bearish'
            confidence = min(abs(score) * 15, 100)
        else:
            bias = 'neutral'
            confidence = 30
        
        return {'bias': bias, 'score': score, 'confidence': confidence, 'factors': factors}
    
    def generate_trade_setup(self, analysis: Dict) -> Optional[Dict]:
        """Generate specific trade setup if conditions align."""
        bias = analysis['bias']
        
        if bias['confidence'] < 50:
            return None
        
        current = analysis['structure']['current_price']
        atr = analysis.get('atr', current * 0.01)
        
        if bias['bias'] == 'bullish':
            support = analysis['structure'].get('nearest_support')
            stop = support['level'] * 0.998 if support else current - atr * 1.5
            resistance = analysis['structure'].get('nearest_resistance')
            risk = current - stop
            target = resistance['level'] if resistance else current + risk * 2
            
            return {
                'action': 'BUY', 'entry': current, 'stop_loss': stop, 'take_profit': target,
                'risk_pips': (current - stop) * 10000, 'reward_pips': (target - current) * 10000,
                'risk_reward': (target - current) / (current - stop) if current != stop else 0
            }
        
        elif bias['bias'] == 'bearish':
            resistance = analysis['structure'].get('nearest_resistance')
            stop = resistance['level'] * 1.002 if resistance else current + atr * 1.5
            support = analysis['structure'].get('nearest_support')
            risk = stop - current
            target = support['level'] if support else current - risk * 2
            
            return {
                'action': 'SELL', 'entry': current, 'stop_loss': stop, 'take_profit': target,
                'risk_pips': (stop - current) * 10000, 'reward_pips': (current - target) * 10000,
                'risk_reward': (current - target) / (stop - current) if stop != current else 0
            }
        
        return None
    
    def full_analysis(self, df: pd.DataFrame) -> Dict:
        """Run complete technical analysis."""
        atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1]
        
        structure = self.analyze_structure(df)
        trend = self.analyze_trend(df)
        patterns = {
            'chart_patterns': self.pattern_detector.scan_patterns(df),
            'candle_patterns': self.candle_scanner.scan_patterns(df, lookback=5)
        }
        
        if 'timestamp' in df.columns:
            df_indexed = df.set_index('timestamp')
        else:
            df_indexed = df
        daily = df_indexed.resample('D').agg({
            'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'
        }).dropna()
        
        if len(daily) >= 2:
            yesterday = daily.iloc[-2]
            pivots = self.pivot_calc.standard_pivots(yesterday['high'], yesterday['low'], yesterday['close'])
        else:
            pivots = {}
        
        bias = self.calculate_bias(structure, trend, patterns, pivots)
        
        analysis = {
            'pair': self.pair, 'timestamp': pd.Timestamp.now(),
            'structure': structure, 'trend': trend, 'patterns': patterns,
            'pivots': pivots, 'bias': bias, 'atr': atr
        }
        
        analysis['trade_setup'] = self.generate_trade_setup(analysis)
        return analysis
    
    def print_report(self, analysis: Dict) -> None:
        """Print formatted analysis report."""
        print("\n" + "=" * 60)
        print(f"  TECHNICAL ANALYSIS: {analysis['pair']}")
        print(f"  {analysis['timestamp'].strftime('%Y-%m-%d %H:%M')}")
        print("=" * 60)
        
        print(f"\nCurrent Price: {analysis['structure']['current_price']:.5f}")
        if analysis['structure']['nearest_resistance']:
            r = analysis['structure']['nearest_resistance']
            print(f"Nearest Resistance: {r['level']:.5f} ({r['strength']})")
        if analysis['structure']['nearest_support']:
            s = analysis['structure']['nearest_support']
            print(f"Nearest Support: {s['level']:.5f} ({s['strength']})")
        
        print(f"\nTrend: {analysis['trend']['direction'].upper()} ({analysis['trend']['strength']}%)")
        
        if analysis['pivots']:
            print(f"\nPivot Points:")
            for level in ['R2', 'R1', 'PP', 'S1', 'S2']:
                if level in analysis['pivots']:
                    print(f"  {level}: {analysis['pivots'][level]:.5f}")
        
        print(f"\n" + "-" * 40)
        print(f"OVERALL BIAS: {analysis['bias']['bias'].upper()}")
        print(f"Confidence: {analysis['bias']['confidence']:.0f}%")
        print(f"Factors: {', '.join(analysis['bias']['factors'][:3])}")
        
        if analysis['trade_setup']:
            setup = analysis['trade_setup']
            print(f"\n" + "=" * 40)
            print(f"TRADE SETUP: {setup['action']}")
            print(f"Entry:  {setup['entry']:.5f}")
            print(f"Stop:   {setup['stop_loss']:.5f} ({setup['risk_pips']:.1f} pips)")
            print(f"Target: {setup['take_profit']:.5f} ({setup['reward_pips']:.1f} pips)")
            print(f"Risk/Reward: {setup['risk_reward']:.2f}")
        else:
            print(f"\nNo trade setup - conditions not aligned")
        
        print("\n" + "=" * 60)


# Run the complete analysis
analyzer = ForexTechnicalAnalyzer(pair='EURUSD')
analysis = analyzer.full_analysis(demo_df)
analyzer.print_report(analysis)

Key Takeaways

  • Support/Resistance: Identify key levels where price historically reverses
  • Trend Lines: Connect swing points to visualize trend direction and breakouts
  • Chart Patterns: Double tops/bottoms, triangles signal potential reversals or continuations
  • Currency Strength: Compare relative strength across pairs for high-probability trades
  • Pivot Points: Daily levels that act as support/resistance for intraday trading
  • Fibonacci: Identify retracement zones and extension targets
  • Multi-Timeframe: Align higher timeframe trend with lower timeframe entries
  • Price Action: Trade candlestick patterns at key levels without indicators
  • Confluence: Combine multiple methods for higher confidence signals

Next: Module 6 - Fundamental Analysis where we'll analyze economic indicators, central bank policies, and build economic calendars for forex trading.

Module 6: Fundamental Analysis for Forex & Futures

Part 2: Analysis & Strategies

Duration Exercises
~2.5 hours 6

Learning Objectives

  • Understand key economic indicators and their impact on currencies
  • Track central bank policies and interest rate decisions
  • Build and use economic calendars for trading
  • Analyze intermarket relationships between currencies, bonds, and commodities

Prerequisites

  • Modules 1-5 (Forex/Futures fundamentals, technical analysis)
  • Basic understanding of macroeconomics
  • Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')

6.1 Economic Indicators

Economic indicators are statistics that provide insights into a country's economic health. They directly impact currency valuations and are essential for fundamental forex analysis.

Types of Economic Indicators

Type Description Examples
Leading Predict future economic activity PMI, Building Permits, Yield Curve
Coincident Reflect current economic state GDP, Employment, Retail Sales
Lagging Confirm trends after they occur Unemployment Rate, CPI, Interest Rates
class EconomicIndicator:
    """Represents an economic indicator with its properties and impact."""
    
    IMPACT_LEVELS = {'high': 3, 'medium': 2, 'low': 1}
    
    def __init__(self, name: str, country: str, impact: str = 'medium'):
        self.name = name
        self.country = country
        self.impact = impact
        self.history: List[Dict] = []
    
    def add_release(self, date: datetime, actual: float, 
                    forecast: float, previous: float) -> None:
        """Add a new data release."""
        surprise = actual - forecast
        surprise_pct = (surprise / abs(forecast) * 100) if forecast != 0 else 0
        
        self.history.append({
            'date': date,
            'actual': actual,
            'forecast': forecast,
            'previous': previous,
            'surprise': surprise,
            'surprise_pct': surprise_pct,
            'beat': actual > forecast
        })
    
    def get_trend(self, periods: int = 4) -> str:
        """Determine recent trend in indicator."""
        if len(self.history) < periods:
            return 'insufficient_data'
        
        recent = self.history[-periods:]
        values = [r['actual'] for r in recent]
        
        increases = sum(1 for i in range(1, len(values)) if values[i] > values[i-1])
        
        if increases >= periods - 1:
            return 'improving'
        elif increases <= 1:
            return 'deteriorating'
        return 'mixed'
    
    def surprise_history(self) -> Dict:
        """Analyze history of forecast surprises."""
        if not self.history:
            return {}
        
        surprises = [r['surprise_pct'] for r in self.history]
        beats = sum(1 for r in self.history if r['beat'])
        
        return {
            'avg_surprise': np.mean(surprises),
            'beat_rate': beats / len(self.history) * 100,
            'largest_beat': max(surprises),
            'largest_miss': min(surprises)
        }

# Demo: US Non-Farm Payrolls
nfp = EconomicIndicator('Non-Farm Payrolls', 'US', 'high')

# Add historical releases
nfp.add_release(datetime(2024, 1, 5), 216, 175, 173)
nfp.add_release(datetime(2024, 2, 2), 353, 185, 216)
nfp.add_release(datetime(2024, 3, 8), 275, 200, 353)
nfp.add_release(datetime(2024, 4, 5), 303, 212, 275)

print(f"Indicator: {nfp.name} ({nfp.country})")
print(f"Impact: {nfp.impact}")
print(f"Trend: {nfp.get_trend()}")
print(f"\nSurprise Analysis:")
for k, v in nfp.surprise_history().items():
    print(f"  {k}: {v:.1f}")

Key Economic Indicators for Forex

GDP (Gross Domestic Product)

  • Measures total economic output
  • Released quarterly
  • Higher GDP growth = stronger currency (generally)

Inflation (CPI/PCE)

  • Consumer Price Index measures price changes
  • Higher inflation may lead to rate hikes = currency strength
  • Too high inflation can weaken confidence

Employment

  • Non-Farm Payrolls (US) - most watched indicator
  • Unemployment Rate
  • Strong employment = strong economy = strong currency
class EconomicIndicatorTracker:
    """Track multiple economic indicators for a country."""
    
    # Standard indicators by importance
    STANDARD_INDICATORS = {
        'US': [
            ('Non-Farm Payrolls', 'high'),
            ('CPI YoY', 'high'),
            ('GDP QoQ', 'high'),
            ('Fed Interest Rate', 'high'),
            ('Retail Sales MoM', 'medium'),
            ('ISM Manufacturing PMI', 'medium'),
            ('Unemployment Rate', 'medium'),
        ],
        'EU': [
            ('ECB Interest Rate', 'high'),
            ('CPI YoY', 'high'),
            ('GDP QoQ', 'high'),
            ('German ZEW Sentiment', 'medium'),
            ('Unemployment Rate', 'medium'),
        ],
        'UK': [
            ('BoE Interest Rate', 'high'),
            ('CPI YoY', 'high'),
            ('GDP QoQ', 'high'),
            ('Retail Sales', 'medium'),
        ],
        'JP': [
            ('BoJ Interest Rate', 'high'),
            ('CPI YoY', 'medium'),
            ('GDP QoQ', 'medium'),
            ('Tankan Survey', 'medium'),
        ]
    }
    
    def __init__(self, country: str):
        self.country = country
        self.indicators: Dict[str, EconomicIndicator] = {}
        self._initialize_indicators()
    
    def _initialize_indicators(self) -> None:
        """Initialize standard indicators for country."""
        if self.country in self.STANDARD_INDICATORS:
            for name, impact in self.STANDARD_INDICATORS[self.country]:
                self.indicators[name] = EconomicIndicator(name, self.country, impact)
    
    def update_indicator(self, name: str, date: datetime,
                        actual: float, forecast: float, previous: float) -> None:
        """Update an indicator with new data."""
        if name in self.indicators:
            self.indicators[name].add_release(date, actual, forecast, previous)
    
    def get_economic_score(self) -> Dict:
        """Calculate overall economic health score."""
        total_score = 0
        max_score = 0
        details = []
        
        for name, indicator in self.indicators.items():
            weight = EconomicIndicator.IMPACT_LEVELS[indicator.impact]
            max_score += weight * 100
            
            if indicator.history:
                latest = indicator.history[-1]
                # Score based on beat/miss and trend
                score = 50  # neutral base
                if latest['beat']:
                    score += min(latest['surprise_pct'] * 2, 30)
                else:
                    score -= min(abs(latest['surprise_pct']) * 2, 30)
                
                trend = indicator.get_trend()
                if trend == 'improving':
                    score += 20
                elif trend == 'deteriorating':
                    score -= 20
                
                score = max(0, min(100, score))
                total_score += score * weight
                
                details.append({
                    'indicator': name,
                    'score': score,
                    'trend': trend,
                    'last_beat': latest['beat']
                })
        
        return {
            'country': self.country,
            'overall_score': (total_score / max_score * 100) if max_score > 0 else 0,
            'interpretation': self._interpret_score(total_score / max_score * 100 if max_score > 0 else 0),
            'details': details
        }
    
    def _interpret_score(self, score: float) -> str:
        """Interpret the economic score."""
        if score >= 70:
            return 'strong_bullish'
        elif score >= 55:
            return 'bullish'
        elif score >= 45:
            return 'neutral'
        elif score >= 30:
            return 'bearish'
        return 'strong_bearish'

# Demo
us_tracker = EconomicIndicatorTracker('US')

# Add some data
us_tracker.update_indicator('Non-Farm Payrolls', datetime(2024, 4, 5), 303, 212, 275)
us_tracker.update_indicator('CPI YoY', datetime(2024, 4, 10), 3.5, 3.4, 3.2)
us_tracker.update_indicator('GDP QoQ', datetime(2024, 4, 25), 1.6, 2.5, 3.4)

score = us_tracker.get_economic_score()
print(f"US Economic Score: {score['overall_score']:.1f}")
print(f"Interpretation: {score['interpretation']}")

6.2 Central Banks

Central banks are the most influential institutions for currency markets. Their monetary policy decisions directly impact currency valuations.

Major Central Banks

Central Bank Currency Key Policy Tool
Federal Reserve (Fed) USD Federal Funds Rate
European Central Bank (ECB) EUR Main Refinancing Rate
Bank of England (BoE) GBP Bank Rate
Bank of Japan (BoJ) JPY Policy Rate, YCC
Swiss National Bank (SNB) CHF Policy Rate
Reserve Bank of Australia (RBA) AUD Cash Rate
Bank of Canada (BoC) CAD Overnight Rate
Reserve Bank of New Zealand (RBNZ) NZD Official Cash Rate
class CentralBank:
    """Model a central bank and its monetary policy."""
    
    def __init__(self, name: str, currency: str, current_rate: float):
        self.name = name
        self.currency = currency
        self.current_rate = current_rate
        self.rate_history: List[Dict] = []
        self.statements: List[Dict] = []
    
    def add_rate_decision(self, date: datetime, rate: float, 
                         expected: float, statement_tone: str = 'neutral') -> None:
        """Record a rate decision."""
        change = rate - self.current_rate
        surprise = rate - expected
        
        self.rate_history.append({
            'date': date,
            'rate': rate,
            'previous': self.current_rate,
            'change': change,
            'expected': expected,
            'surprise': surprise,
            'action': 'hike' if change > 0 else ('cut' if change < 0 else 'hold'),
            'tone': statement_tone
        })
        self.current_rate = rate
    
    def get_policy_stance(self) -> str:
        """Determine current policy stance."""
        if len(self.rate_history) < 2:
            return 'unknown'
        
        recent = self.rate_history[-3:] if len(self.rate_history) >= 3 else self.rate_history
        hikes = sum(1 for r in recent if r['action'] == 'hike')
        cuts = sum(1 for r in recent if r['action'] == 'cut')
        
        if hikes > cuts:
            return 'hawkish'
        elif cuts > hikes:
            return 'dovish'
        return 'neutral'
    
    def rate_differential(self, other: 'CentralBank') -> float:
        """Calculate rate differential with another central bank."""
        return self.current_rate - other.current_rate
    
    def expected_path(self, meetings: int = 4) -> List[Dict]:
        """Project expected rate path based on recent trend."""
        stance = self.get_policy_stance()
        path = []
        rate = self.current_rate
        
        for i in range(meetings):
            if stance == 'hawkish':
                rate += 0.25 if i < 2 else 0
            elif stance == 'dovish':
                rate -= 0.25 if i < 2 else 0
            
            path.append({
                'meeting': i + 1,
                'projected_rate': rate
            })
        
        return path

# Demo
fed = CentralBank('Federal Reserve', 'USD', 5.25)
ecb = CentralBank('European Central Bank', 'EUR', 4.50)

# Add rate history
fed.add_rate_decision(datetime(2024, 1, 31), 5.25, 5.25, 'neutral')
fed.add_rate_decision(datetime(2024, 3, 20), 5.25, 5.25, 'hawkish')

ecb.add_rate_decision(datetime(2024, 1, 25), 4.50, 4.50, 'neutral')
ecb.add_rate_decision(datetime(2024, 3, 7), 4.50, 4.50, 'dovish')

print(f"Fed current rate: {fed.current_rate}%")
print(f"Fed stance: {fed.get_policy_stance()}")
print(f"\nECB current rate: {ecb.current_rate}%")
print(f"ECB stance: {ecb.get_policy_stance()}")
print(f"\nUSD-EUR rate differential: {fed.rate_differential(ecb):.2f}%")
class CentralBankMonitor:
    """Monitor multiple central banks for trading signals."""
    
    CURRENCY_PAIRS = {
        ('USD', 'EUR'): 'EURUSD',
        ('USD', 'GBP'): 'GBPUSD',
        ('USD', 'JPY'): 'USDJPY',
        ('USD', 'CHF'): 'USDCHF',
        ('USD', 'AUD'): 'AUDUSD',
        ('USD', 'CAD'): 'USDCAD',
        ('EUR', 'GBP'): 'EURGBP',
        ('EUR', 'JPY'): 'EURJPY',
    }
    
    def __init__(self):
        self.banks: Dict[str, CentralBank] = {}
    
    def add_bank(self, bank: CentralBank) -> None:
        """Add a central bank to monitor."""
        self.banks[bank.currency] = bank
    
    def get_all_differentials(self) -> pd.DataFrame:
        """Calculate all rate differentials."""
        data = []
        currencies = list(self.banks.keys())
        
        for i, curr1 in enumerate(currencies):
            for curr2 in currencies[i+1:]:
                bank1 = self.banks[curr1]
                bank2 = self.banks[curr2]
                diff = bank1.rate_differential(bank2)
                
                pair_key = (curr1, curr2) if (curr1, curr2) in self.CURRENCY_PAIRS else (curr2, curr1)
                pair = self.CURRENCY_PAIRS.get(pair_key, f"{curr1}{curr2}")
                
                data.append({
                    'pair': pair,
                    'currency_1': curr1,
                    'currency_2': curr2,
                    'rate_1': bank1.current_rate,
                    'rate_2': bank2.current_rate,
                    'differential': diff,
                    'carry_direction': curr1 if diff > 0 else curr2
                })
        
        return pd.DataFrame(data)
    
    def policy_divergence_signals(self) -> List[Dict]:
        """Find trading opportunities from policy divergence."""
        signals = []
        currencies = list(self.banks.keys())
        
        for i, curr1 in enumerate(currencies):
            for curr2 in currencies[i+1:]:
                bank1 = self.banks[curr1]
                bank2 = self.banks[curr2]
                
                stance1 = bank1.get_policy_stance()
                stance2 = bank2.get_policy_stance()
                
                # Look for divergence
                if stance1 == 'hawkish' and stance2 == 'dovish':
                    signals.append({
                        'long': curr1,
                        'short': curr2,
                        'reason': f'{bank1.name} hawkish vs {bank2.name} dovish',
                        'strength': 'strong'
                    })
                elif stance1 == 'dovish' and stance2 == 'hawkish':
                    signals.append({
                        'long': curr2,
                        'short': curr1,
                        'reason': f'{bank2.name} hawkish vs {bank1.name} dovish',
                        'strength': 'strong'
                    })
        
        return signals

# Demo
monitor = CentralBankMonitor()
monitor.add_bank(fed)
monitor.add_bank(ecb)
monitor.add_bank(CentralBank('Bank of Japan', 'JPY', 0.10))

print("Rate Differentials:")
print(monitor.get_all_differentials().to_string(index=False))

print("\nPolicy Divergence Signals:")
for signal in monitor.policy_divergence_signals():
    print(f"  Long {signal['long']}, Short {signal['short']}")
    print(f"  Reason: {signal['reason']}")

6.3 Economic Calendar

An economic calendar tracks scheduled data releases and events. It's essential for avoiding unexpected volatility and finding trading opportunities.

from enum import Enum

class EventImpact(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

@dataclass
class CalendarEvent:
    """Represents an economic calendar event."""
    name: str
    country: str
    datetime: datetime
    impact: EventImpact
    forecast: Optional[float] = None
    previous: Optional[float] = None
    actual: Optional[float] = None
    
    @property
    def is_released(self) -> bool:
        return self.actual is not None
    
    @property
    def surprise(self) -> Optional[float]:
        if self.actual is not None and self.forecast is not None:
            return self.actual - self.forecast
        return None
    
    @property
    def beat_forecast(self) -> Optional[bool]:
        if self.surprise is not None:
            return self.surprise > 0
        return None

# Demo
nfp_event = CalendarEvent(
    name='Non-Farm Payrolls',
    country='US',
    datetime=datetime(2024, 5, 3, 8, 30),
    impact=EventImpact.HIGH,
    forecast=240.0,
    previous=303.0
)

print(f"Event: {nfp_event.name}")
print(f"Time: {nfp_event.datetime}")
print(f"Impact: {nfp_event.impact.name}")
print(f"Forecast: {nfp_event.forecast}K")
print(f"Released: {nfp_event.is_released}")
class EconomicCalendar:
    """Manage economic calendar events."""
    
    # Standard high-impact events
    HIGH_IMPACT_EVENTS = {
        'US': ['Non-Farm Payrolls', 'CPI', 'FOMC Rate Decision', 'GDP', 'Retail Sales'],
        'EU': ['ECB Rate Decision', 'CPI', 'GDP', 'German ZEW'],
        'UK': ['BoE Rate Decision', 'CPI', 'GDP', 'Employment'],
        'JP': ['BoJ Rate Decision', 'CPI', 'GDP', 'Tankan'],
        'AU': ['RBA Rate Decision', 'Employment', 'CPI'],
        'CA': ['BoC Rate Decision', 'Employment', 'CPI'],
    }
    
    def __init__(self):
        self.events: List[CalendarEvent] = []
    
    def add_event(self, event: CalendarEvent) -> None:
        """Add event to calendar."""
        self.events.append(event)
        self.events.sort(key=lambda x: x.datetime)
    
    def get_upcoming(self, hours: int = 24, 
                     min_impact: EventImpact = EventImpact.LOW) -> List[CalendarEvent]:
        """Get upcoming events within time window."""
        now = datetime.now()
        cutoff = now + timedelta(hours=hours)
        
        return [
            e for e in self.events
            if now <= e.datetime <= cutoff
            and e.impact.value >= min_impact.value
            and not e.is_released
        ]
    
    def get_by_country(self, country: str) -> List[CalendarEvent]:
        """Get events for specific country."""
        return [e for e in self.events if e.country == country]
    
    def get_high_impact_today(self) -> List[CalendarEvent]:
        """Get today's high-impact events."""
        today = datetime.now().date()
        return [
            e for e in self.events
            if e.datetime.date() == today
            and e.impact == EventImpact.HIGH
        ]
    
    def currency_risk_score(self, currency: str, hours: int = 24) -> Dict:
        """Calculate event risk score for a currency."""
        # Map currencies to countries
        currency_country = {
            'USD': 'US', 'EUR': 'EU', 'GBP': 'UK', 'JPY': 'JP',
            'AUD': 'AU', 'CAD': 'CA', 'CHF': 'CH', 'NZD': 'NZ'
        }
        
        country = currency_country.get(currency, currency)
        upcoming = self.get_upcoming(hours)
        country_events = [e for e in upcoming if e.country == country]
        
        risk_score = sum(e.impact.value for e in country_events)
        
        return {
            'currency': currency,
            'event_count': len(country_events),
            'risk_score': risk_score,
            'high_impact_events': [e.name for e in country_events if e.impact == EventImpact.HIGH],
            'recommendation': 'avoid' if risk_score >= 5 else ('caution' if risk_score >= 3 else 'normal')
        }
    
    def to_dataframe(self) -> pd.DataFrame:
        """Convert calendar to DataFrame."""
        return pd.DataFrame([
            {
                'datetime': e.datetime,
                'country': e.country,
                'event': e.name,
                'impact': e.impact.name,
                'forecast': e.forecast,
                'previous': e.previous,
                'actual': e.actual
            }
            for e in self.events
        ])

# Demo: Create sample calendar
calendar = EconomicCalendar()

# Add some events
base_date = datetime.now().replace(hour=8, minute=30, second=0, microsecond=0)

calendar.add_event(CalendarEvent(
    'Non-Farm Payrolls', 'US', base_date + timedelta(hours=2),
    EventImpact.HIGH, forecast=240, previous=303
))
calendar.add_event(CalendarEvent(
    'ECB Rate Decision', 'EU', base_date + timedelta(hours=5),
    EventImpact.HIGH, forecast=4.50, previous=4.50
))
calendar.add_event(CalendarEvent(
    'US Retail Sales', 'US', base_date + timedelta(hours=3),
    EventImpact.MEDIUM, forecast=0.4, previous=0.6
))
calendar.add_event(CalendarEvent(
    'German ZEW', 'EU', base_date + timedelta(hours=1),
    EventImpact.MEDIUM, forecast=42.5, previous=38.4
))

print("Upcoming High Impact Events (24h):")
for event in calendar.get_upcoming(24, EventImpact.HIGH):
    print(f"  {event.datetime.strftime('%H:%M')} - {event.country}: {event.name}")

print("\nUSD Risk Assessment:")
risk = calendar.currency_risk_score('USD')
print(f"  Events: {risk['event_count']}")
print(f"  Risk Score: {risk['risk_score']}")
print(f"  Recommendation: {risk['recommendation']}")

6.4 Intermarket Analysis

Intermarket analysis examines relationships between different asset classes to gain insights into currency movements.

Key Intermarket Relationships

Relationship Correlation Explanation
USD vs Gold Negative Gold priced in USD; weak USD = expensive gold
AUD vs Gold Positive Australia major gold exporter
CAD vs Oil Positive Canada major oil exporter
USD vs US Yields Positive Higher yields attract USD investment
JPY vs Risk Negative JPY is safe haven; strengthens in risk-off
class IntermarketAnalyzer:
    """Analyze relationships between currencies and other markets."""
    
    # Known correlations (positive = move together, negative = inverse)
    KNOWN_CORRELATIONS = {
        ('AUD', 'GOLD'): 0.7,
        ('CAD', 'OIL'): 0.6,
        ('USD', 'GOLD'): -0.5,
        ('USD', 'US10Y'): 0.6,
        ('JPY', 'VIX'): 0.5,  # Safe haven
        ('CHF', 'VIX'): 0.4,  # Safe haven
        ('AUD', 'SPX'): 0.5,  # Risk currency
        ('NZD', 'SPX'): 0.4,  # Risk currency
    }
    
    def __init__(self):
        self.price_data: Dict[str, pd.Series] = {}
    
    def add_data(self, symbol: str, prices: pd.Series) -> None:
        """Add price data for an asset."""
        self.price_data[symbol] = prices
    
    def calculate_returns(self, symbol: str, period: int = 1) -> pd.Series:
        """Calculate returns for an asset."""
        if symbol not in self.price_data:
            return pd.Series()
        return self.price_data[symbol].pct_change(period)
    
    def calculate_correlation(self, symbol1: str, symbol2: str, 
                             window: int = 20) -> float:
        """Calculate rolling correlation between two assets."""
        if symbol1 not in self.price_data or symbol2 not in self.price_data:
            return 0.0
        
        returns1 = self.calculate_returns(symbol1)
        returns2 = self.calculate_returns(symbol2)
        
        # Align data
        aligned = pd.concat([returns1, returns2], axis=1).dropna()
        if len(aligned) < window:
            return 0.0
        
        return aligned.iloc[:, 0].corr(aligned.iloc[:, 1])
    
    def get_correlation_matrix(self) -> pd.DataFrame:
        """Get correlation matrix for all assets."""
        symbols = list(self.price_data.keys())
        returns_df = pd.DataFrame({
            sym: self.calculate_returns(sym) for sym in symbols
        }).dropna()
        
        return returns_df.corr()
    
    def check_divergence(self, currency: str, related_asset: str,
                        lookback: int = 10) -> Optional[Dict]:
        """Check for divergence between currency and related asset."""
        expected_corr = self.KNOWN_CORRELATIONS.get((currency, related_asset), 0)
        
        if expected_corr == 0:
            return None
        
        actual_corr = self.calculate_correlation(currency, related_asset, lookback)
        
        # Check if correlation has flipped or weakened significantly
        if expected_corr > 0 and actual_corr < 0:
            divergence = 'negative_flip'
        elif expected_corr < 0 and actual_corr > 0:
            divergence = 'positive_flip'
        elif abs(actual_corr) < abs(expected_corr) * 0.5:
            divergence = 'weakened'
        else:
            divergence = 'normal'
        
        return {
            'currency': currency,
            'related_asset': related_asset,
            'expected_correlation': expected_corr,
            'actual_correlation': actual_corr,
            'divergence': divergence,
            'signal': divergence != 'normal'
        }
    
    def generate_signals(self) -> List[Dict]:
        """Generate trading signals from intermarket analysis."""
        signals = []
        
        for (currency, asset), expected_corr in self.KNOWN_CORRELATIONS.items():
            if currency in self.price_data and asset in self.price_data:
                divergence = self.check_divergence(currency, asset)
                if divergence and divergence['signal']:
                    signals.append(divergence)
        
        return signals

# Demo with simulated data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')

analyzer = IntermarketAnalyzer()

# Simulate correlated data
gold = pd.Series([2000 + np.cumsum(np.random.normal(0, 10, 60))[i] for i in range(60)], index=dates)
aud = pd.Series([0.65 + np.cumsum(np.random.normal(0, 0.002, 60))[i] + gold.pct_change().fillna(0).cumsum()[i] * 0.3 for i in range(60)], index=dates)
oil = pd.Series([75 + np.cumsum(np.random.normal(0, 1, 60))[i] for i in range(60)], index=dates)
cad = pd.Series([1.35 + np.cumsum(np.random.normal(0, 0.002, 60))[i] - oil.pct_change().fillna(0).cumsum()[i] * 0.2 for i in range(60)], index=dates)

analyzer.add_data('GOLD', gold)
analyzer.add_data('AUD', aud)
analyzer.add_data('OIL', oil)
analyzer.add_data('CAD', cad)

print("Correlation Matrix:")
print(analyzer.get_correlation_matrix().round(2))

print("\nAUD-Gold Divergence Check:")
div = analyzer.check_divergence('AUD', 'GOLD')
if div:
    print(f"  Expected: {div['expected_correlation']:.2f}")
    print(f"  Actual: {div['actual_correlation']:.2f}")
    print(f"  Status: {div['divergence']}")
class BondCurrencyAnalyzer:
    """Analyze bond yields and currency relationships."""
    
    def __init__(self):
        self.yields: Dict[str, pd.Series] = {}
        self.currencies: Dict[str, pd.Series] = {}
    
    def add_yield(self, country: str, yields: pd.Series) -> None:
        """Add bond yield data."""
        self.yields[country] = yields
    
    def add_currency(self, pair: str, prices: pd.Series) -> None:
        """Add currency pair data."""
        self.currencies[pair] = prices
    
    def yield_spread(self, country1: str, country2: str) -> pd.Series:
        """Calculate yield spread between two countries."""
        if country1 in self.yields and country2 in self.yields:
            return self.yields[country1] - self.yields[country2]
        return pd.Series()
    
    def spread_correlation(self, country1: str, country2: str, 
                          pair: str, window: int = 20) -> float:
        """Calculate correlation between yield spread and currency pair."""
        spread = self.yield_spread(country1, country2)
        currency = self.currencies.get(pair, pd.Series())
        
        if spread.empty or currency.empty:
            return 0.0
        
        aligned = pd.concat([spread, currency], axis=1).dropna()
        if len(aligned) < window:
            return 0.0
        
        return aligned.iloc[:, 0].corr(aligned.iloc[:, 1])
    
    def analyze_carry_opportunity(self, high_yield: str, low_yield: str,
                                  pair: str) -> Dict:
        """Analyze carry trade opportunity."""
        if high_yield not in self.yields or low_yield not in self.yields:
            return {}
        
        spread = self.yield_spread(high_yield, low_yield)
        current_spread = spread.iloc[-1] if len(spread) > 0 else 0
        avg_spread = spread.mean() if len(spread) > 0 else 0
        
        # Check if spread is widening or narrowing
        recent_change = spread.iloc[-1] - spread.iloc[-5] if len(spread) >= 5 else 0
        
        return {
            'pair': pair,
            'high_yield_country': high_yield,
            'low_yield_country': low_yield,
            'current_spread': current_spread,
            'average_spread': avg_spread,
            'spread_vs_avg': current_spread - avg_spread,
            'recent_trend': 'widening' if recent_change > 0 else 'narrowing',
            'carry_attractive': current_spread > avg_spread and recent_change > 0
        }

# Demo
bond_analyzer = BondCurrencyAnalyzer()

# Simulate yield data
dates = pd.date_range('2024-01-01', periods=60, freq='D')
us_yields = pd.Series([4.5 + np.random.normal(0, 0.05) for _ in range(60)], index=dates)
jp_yields = pd.Series([0.1 + np.random.normal(0, 0.02) for _ in range(60)], index=dates)
eu_yields = pd.Series([3.0 + np.random.normal(0, 0.04) for _ in range(60)], index=dates)

bond_analyzer.add_yield('US', us_yields)
bond_analyzer.add_yield('JP', jp_yields)
bond_analyzer.add_yield('EU', eu_yields)

# Currency data
usdjpy = pd.Series([150 + np.random.normal(0, 0.5) for _ in range(60)], index=dates)
bond_analyzer.add_currency('USDJPY', usdjpy)

print("US-JP Yield Spread Analysis:")
spread = bond_analyzer.yield_spread('US', 'JP')
print(f"  Current Spread: {spread.iloc[-1]:.2f}%")

print("\nCarry Trade Analysis (Long USD/JPY):")
carry = bond_analyzer.analyze_carry_opportunity('US', 'JP', 'USDJPY')
print(f"  Spread vs Average: {carry['spread_vs_avg']:.2f}%")
print(f"  Recent Trend: {carry['recent_trend']}")
print(f"  Carry Attractive: {carry['carry_attractive']}")

Exercises

Exercise 1: Economic Indicator Analyzer (Guided)

Complete the IndicatorAnalyzer class that tracks economic indicators and generates currency bias.

class IndicatorAnalyzer:
    """Analyze economic indicators for currency bias."""
    
    def __init__(self, country: str):
        self.country = country
        self.indicators: Dict[str, List[Dict]] = {}
    
    def add_reading(self, indicator: str, actual: float, 
                    forecast: float, previous: float) -> None:
        """Add an indicator reading."""
        if indicator not in self.indicators:
            self.indicators[indicator] = []
        
        surprise = actual - forecast
        trend = 'improving' if actual > previous else 'deteriorating'
        
        self.indicators[indicator].append({
            'actual': actual,
            'forecast': forecast,
            'previous': previous,
            'surprise': surprise,
            'beat': actual ______ forecast,
            'trend': trend
        })
    
    def get_beat_rate(self, indicator: str) -> float:
        """Calculate what percentage of releases beat forecast."""
        if indicator not in self.indicators:
            return 0.0
        
        readings = self.indicators[indicator]
        beats = sum(1 for r in readings if r['beat'])
        return (beats / ______(readings)) * 100 if readings else 0.0
    
    def overall_bias(self) -> str:
        """Calculate overall currency bias from all indicators."""
        if not self.indicators:
            return 'neutral'
        
        total_beats = 0
        total_readings = 0
        
        for readings in self.indicators.values():
            if readings:
                latest = readings[-1]
                if latest['beat']:
                    total_beats += 1
                total_readings += 1
        
        if total_readings == 0:
            return 'neutral'
        
        beat_pct = total_beats / total_readings
        
        if beat_pct >= ______:
            return 'bullish'
        elif beat_pct <= 0.3:
            return 'bearish'
        return 'neutral'

# Test
analyzer = IndicatorAnalyzer('US')
analyzer.add_reading('NFP', 303, 212, 275)  # Beat
analyzer.add_reading('CPI', 3.5, 3.4, 3.2)  # Beat
analyzer.add_reading('GDP', 1.6, 2.5, 3.4)  # Miss

print(f"NFP Beat Rate: {analyzer.get_beat_rate('NFP'):.1f}%")
print(f"Overall Bias: {analyzer.overall_bias()}")
Solution 1
class IndicatorAnalyzer:
    """Analyze economic indicators for currency bias."""

    def __init__(self, country: str):
        self.country = country
        self.indicators: Dict[str, List[Dict]] = {}

    def add_reading(self, indicator: str, actual: float, 
                    forecast: float, previous: float) -> None:
        """Add an indicator reading."""
        if indicator not in self.indicators:
            self.indicators[indicator] = []

        surprise = actual - forecast
        trend = 'improving' if actual > previous else 'deteriorating'

        self.indicators[indicator].append({
            'actual': actual,
            'forecast': forecast,
            'previous': previous,
            'surprise': surprise,
            'beat': actual > forecast,
            'trend': trend
        })

    def get_beat_rate(self, indicator: str) -> float:
        """Calculate what percentage of releases beat forecast."""
        if indicator not in self.indicators:
            return 0.0

        readings = self.indicators[indicator]
        beats = sum(1 for r in readings if r['beat'])
        return (beats / len(readings)) * 100 if readings else 0.0

    def overall_bias(self) -> str:
        """Calculate overall currency bias from all indicators."""
        if not self.indicators:
            return 'neutral'

        total_beats = 0
        total_readings = 0

        for readings in self.indicators.values():
            if readings:
                latest = readings[-1]
                if latest['beat']:
                    total_beats += 1
                total_readings += 1

        if total_readings == 0:
            return 'neutral'

        beat_pct = total_beats / total_readings

        if beat_pct >= 0.6:
            return 'bullish'
        elif beat_pct <= 0.3:
            return 'bearish'
        return 'neutral'

Exercise 2: Central Bank Rate Tracker (Guided)

Complete the RateTracker class that monitors central bank rates and calculates rate differentials.

class RateTracker:
    """Track central bank interest rates."""
    
    def __init__(self):
        self.rates: Dict[str, float] = {}
        self.history: Dict[str, List[Tuple[datetime, float]]] = {}
    
    def set_rate(self, currency: str, rate: float, date: datetime = None) -> None:
        """Set current rate for a currency."""
        self.rates[currency] = rate
        
        if currency not in self.history:
            self.history[currency] = []
        self.history[currency].______((
            date or datetime.now(),
            rate
        ))
    
    def get_differential(self, curr1: str, curr2: str) -> float:
        """Get rate differential between two currencies."""
        rate1 = self.rates.get(curr1, 0)
        rate2 = self.rates.get(curr2, 0)
        return rate1 ______ rate2
    
    def get_highest_yielding(self) -> Tuple[str, float]:
        """Get currency with highest yield."""
        if not self.rates:
            return ('', 0.0)
        return ______(self.rates.items(), key=lambda x: x[1])
    
    def get_carry_pairs(self) -> List[Dict]:
        """Get best carry trade pairs (long high yield, short low yield)."""
        pairs = []
        currencies = list(self.rates.keys())
        
        for i, c1 in enumerate(currencies):
            for c2 in currencies[i+1:]:
                diff = self.get_differential(c1, c2)
                if abs(diff) >= 1.0:  # Minimum 1% differential
                    pairs.append({
                        'long': c1 if diff > 0 else c2,
                        'short': c2 if diff > 0 else c1,
                        'differential': abs(diff)
                    })
        
        return sorted(pairs, key=lambda x: x['differential'], reverse=True)

# Test
tracker = RateTracker()
tracker.set_rate('USD', 5.25)
tracker.set_rate('EUR', 4.50)
tracker.set_rate('JPY', 0.10)
tracker.set_rate('GBP', 5.00)

print(f"USD-JPY Differential: {tracker.get_differential('USD', 'JPY'):.2f}%")
print(f"Highest Yielding: {tracker.get_highest_yielding()}")
print(f"\nBest Carry Pairs:")
for pair in tracker.get_carry_pairs()[:3]:
    print(f"  Long {pair['long']}/Short {pair['short']}: {pair['differential']:.2f}%")
Solution 2
class RateTracker:
    """Track central bank interest rates."""

    def __init__(self):
        self.rates: Dict[str, float] = {}
        self.history: Dict[str, List[Tuple[datetime, float]]] = {}

    def set_rate(self, currency: str, rate: float, date: datetime = None) -> None:
        """Set current rate for a currency."""
        self.rates[currency] = rate

        if currency not in self.history:
            self.history[currency] = []
        self.history[currency].append((
            date or datetime.now(),
            rate
        ))

    def get_differential(self, curr1: str, curr2: str) -> float:
        """Get rate differential between two currencies."""
        rate1 = self.rates.get(curr1, 0)
        rate2 = self.rates.get(curr2, 0)
        return rate1 - rate2

    def get_highest_yielding(self) -> Tuple[str, float]:
        """Get currency with highest yield."""
        if not self.rates:
            return ('', 0.0)
        return max(self.rates.items(), key=lambda x: x[1])

    def get_carry_pairs(self) -> List[Dict]:
        """Get best carry trade pairs."""
        pairs = []
        currencies = list(self.rates.keys())

        for i, c1 in enumerate(currencies):
            for c2 in currencies[i+1:]:
                diff = self.get_differential(c1, c2)
                if abs(diff) >= 1.0:
                    pairs.append({
                        'long': c1 if diff > 0 else c2,
                        'short': c2 if diff > 0 else c1,
                        'differential': abs(diff)
                    })

        return sorted(pairs, key=lambda x: x['differential'], reverse=True)

Exercise 3: Event Impact Scorer (Guided)

Complete the EventScorer class that scores economic events based on their potential market impact.

class EventScorer:
    """Score economic events for trading decisions."""
    
    BASE_SCORES = {
        'rate_decision': 10,
        'nfp': 9,
        'cpi': 8,
        'gdp': 7,
        'retail_sales': 5,
        'pmi': 4
    }
    
    def __init__(self):
        self.events: List[Dict] = []
    
    def add_event(self, event_type: str, currency: str,
                  surprise_pct: float = 0) -> None:
        """Add event with its surprise magnitude."""
        base_score = self.BASE_SCORES.get(event_type.lower(), 3)
        
        # Adjust score based on surprise magnitude
        surprise_multiplier = 1 + (______(surprise_pct) / 100)
        final_score = base_score * surprise_multiplier
        
        self.events.append({
            'type': event_type,
            'currency': currency,
            'surprise_pct': surprise_pct,
            'score': final_score,
            'direction': 'bullish' if surprise_pct > 0 else ('bearish' if surprise_pct < 0 else 'neutral')
        })
    
    def get_currency_score(self, currency: str) -> Dict:
        """Get aggregate score for a currency."""
        currency_events = [e for e in self.events if e['currency'] == currency]
        
        if not currency_events:
            return {'currency': currency, 'score': 0, 'bias': 'neutral'}
        
        bullish_score = ______(e['score'] for e in currency_events if e['direction'] == 'bullish')
        bearish_score = sum(e['score'] for e in currency_events if e['direction'] == 'bearish')
        
        net_score = bullish_score - bearish_score
        
        return {
            'currency': currency,
            'bullish_score': bullish_score,
            'bearish_score': bearish_score,
            'net_score': net_score,
            'bias': 'bullish' if net_score > 5 else ('______' if net_score < -5 else 'neutral')
        }

# Test
scorer = EventScorer()
scorer.add_event('NFP', 'USD', 15.5)  # Big beat
scorer.add_event('CPI', 'USD', 2.9)   # Small beat
scorer.add_event('GDP', 'USD', -36.0) # Big miss

result = scorer.get_currency_score('USD')
print(f"USD Analysis:")
print(f"  Bullish Score: {result['bullish_score']:.1f}")
print(f"  Bearish Score: {result['bearish_score']:.1f}")
print(f"  Net Score: {result['net_score']:.1f}")
print(f"  Bias: {result['bias']}")
Solution 3
class EventScorer:
    """Score economic events for trading decisions."""

    BASE_SCORES = {
        'rate_decision': 10,
        'nfp': 9,
        'cpi': 8,
        'gdp': 7,
        'retail_sales': 5,
        'pmi': 4
    }

    def __init__(self):
        self.events: List[Dict] = []

    def add_event(self, event_type: str, currency: str,
                  surprise_pct: float = 0) -> None:
        """Add event with its surprise magnitude."""
        base_score = self.BASE_SCORES.get(event_type.lower(), 3)

        surprise_multiplier = 1 + (abs(surprise_pct) / 100)
        final_score = base_score * surprise_multiplier

        self.events.append({
            'type': event_type,
            'currency': currency,
            'surprise_pct': surprise_pct,
            'score': final_score,
            'direction': 'bullish' if surprise_pct > 0 else ('bearish' if surprise_pct < 0 else 'neutral')
        })

    def get_currency_score(self, currency: str) -> Dict:
        """Get aggregate score for a currency."""
        currency_events = [e for e in self.events if e['currency'] == currency]

        if not currency_events:
            return {'currency': currency, 'score': 0, 'bias': 'neutral'}

        bullish_score = sum(e['score'] for e in currency_events if e['direction'] == 'bullish')
        bearish_score = sum(e['score'] for e in currency_events if e['direction'] == 'bearish')

        net_score = bullish_score - bearish_score

        return {
            'currency': currency,
            'bullish_score': bullish_score,
            'bearish_score': bearish_score,
            'net_score': net_score,
            'bias': 'bullish' if net_score > 5 else ('bearish' if net_score < -5 else 'neutral')
        }

Exercise 4: Complete Economic Calendar (Open-ended)

Build a comprehensive economic calendar system that: - Stores events with full details - Filters by date range, country, and impact - Calculates risk scores for trading windows - Generates pre-event alerts

# Exercise 4: Complete Economic Calendar (Open-ended)
#
# Requirements:
# 1. Create class ComprehensiveCalendar
# 2. Store events with: name, country, datetime, impact, forecast, previous, actual
# 3. Methods: add_event, get_events_in_range, filter_by_impact, filter_by_country
# 4. Calculate risk_score for a time window (sum of impact levels)
# 5. Generate alerts for high-impact events in next N hours
#
# Your implementation:
Solution 4
class ComprehensiveCalendar:
    """Full-featured economic calendar."""

    def __init__(self):
        self.events: List[Dict] = []

    def add_event(self, name: str, country: str, dt: datetime,
                  impact: int, forecast: float = None,
                  previous: float = None, actual: float = None) -> None:
        """Add event to calendar."""
        self.events.append({
            'name': name,
            'country': country,
            'datetime': dt,
            'impact': impact,  # 1-3
            'forecast': forecast,
            'previous': previous,
            'actual': actual
        })
        self.events.sort(key=lambda x: x['datetime'])

    def get_events_in_range(self, start: datetime, end: datetime) -> List[Dict]:
        """Get events within date range."""
        return [e for e in self.events if start <= e['datetime'] <= end]

    def filter_by_impact(self, min_impact: int = 2) -> List[Dict]:
        """Filter events by minimum impact level."""
        return [e for e in self.events if e['impact'] >= min_impact]

    def filter_by_country(self, country: str) -> List[Dict]:
        """Filter events by country."""
        return [e for e in self.events if e['country'] == country]

    def risk_score(self, start: datetime, end: datetime) -> int:
        """Calculate risk score for time window."""
        events = self.get_events_in_range(start, end)
        return sum(e['impact'] for e in events)

    def get_alerts(self, hours_ahead: int = 24) -> List[Dict]:
        """Get alerts for high-impact events."""
        now = datetime.now()
        cutoff = now + timedelta(hours=hours_ahead)

        high_impact = [
            e for e in self.events
            if now <= e['datetime'] <= cutoff and e['impact'] >= 3
        ]

        return [{
            'event': e['name'],
            'country': e['country'],
            'time': e['datetime'],
            'hours_until': (e['datetime'] - now).total_seconds() / 3600
        } for e in high_impact]

Exercise 5: Intermarket Correlation Tracker (Open-ended)

Build an intermarket analyzer that: - Tracks correlations between currencies and related assets - Detects correlation breakdowns - Generates divergence signals

# Exercise 5: Intermarket Correlation Tracker (Open-ended)
#
# Requirements:
# 1. Create class CorrelationTracker
# 2. Store price series for multiple assets
# 3. Calculate rolling correlations
# 4. Define expected correlations (e.g., AUD-Gold positive)
# 5. Detect when actual correlation deviates from expected
# 6. Generate trading signals from divergences
#
# Your implementation:
Solution 5
class CorrelationTracker:
    """Track and analyze intermarket correlations."""

    EXPECTED = {
        ('AUD', 'GOLD'): 0.6,
        ('CAD', 'OIL'): 0.5,
        ('USD', 'GOLD'): -0.4,
        ('JPY', 'VIX'): 0.5,
    }

    def __init__(self):
        self.data: Dict[str, pd.Series] = {}

    def add_series(self, symbol: str, prices: pd.Series) -> None:
        """Add price series."""
        self.data[symbol] = prices

    def rolling_correlation(self, sym1: str, sym2: str, window: int = 20) -> pd.Series:
        """Calculate rolling correlation."""
        if sym1 not in self.data or sym2 not in self.data:
            return pd.Series()

        r1 = self.data[sym1].pct_change()
        r2 = self.data[sym2].pct_change()
        return r1.rolling(window).corr(r2)

    def check_divergence(self, sym1: str, sym2: str, threshold: float = 0.3) -> Dict:
        """Check for correlation divergence."""
        expected = self.EXPECTED.get((sym1, sym2), 0)
        if expected == 0:
            expected = self.EXPECTED.get((sym2, sym1), 0)

        actual = self.rolling_correlation(sym1, sym2).iloc[-1]
        deviation = actual - expected

        return {
            'pair': f"{sym1}-{sym2}",
            'expected': expected,
            'actual': actual,
            'deviation': deviation,
            'divergence': abs(deviation) > threshold,
            'signal': 'divergence_detected' if abs(deviation) > threshold else 'normal'
        }

    def scan_all_pairs(self) -> List[Dict]:
        """Scan all known pairs for divergences."""
        signals = []
        for (sym1, sym2) in self.EXPECTED.keys():
            if sym1 in self.data and sym2 in self.data:
                result = self.check_divergence(sym1, sym2)
                if result['divergence']:
                    signals.append(result)
        return signals

Exercise 6: Fundamental Dashboard Builder (Open-ended)

Build a comprehensive fundamental analysis dashboard that combines: - Economic indicators tracking - Central bank monitoring - Economic calendar - Intermarket analysis - Overall currency bias scoring

# Exercise 6: Fundamental Dashboard Builder (Open-ended)
#
# Requirements:
# 1. Create class FundamentalDashboard
# 2. Integrate: indicator tracker, central bank monitor, calendar, intermarket
# 3. Calculate overall fundamental score per currency
# 4. Rank currencies from most bullish to most bearish
# 5. Generate pair recommendations (long strongest vs short weakest)
# 6. Print formatted dashboard report
#
# Your implementation:
Solution 6
class FundamentalDashboard:
    """Comprehensive fundamental analysis dashboard."""

    def __init__(self):
        self.indicators: Dict[str, EconomicIndicatorTracker] = {}
        self.central_banks: Dict[str, CentralBank] = {}
        self.calendar = EconomicCalendar()
        self.intermarket = IntermarketAnalyzer()

    def add_country(self, country: str, currency: str, rate: float) -> None:
        """Add a country to track."""
        self.indicators[currency] = EconomicIndicatorTracker(country)
        self.central_banks[currency] = CentralBank(f"{country} CB", currency, rate)

    def calculate_currency_score(self, currency: str) -> Dict:
        """Calculate overall fundamental score for currency."""
        score = 50  # Neutral base
        factors = []

        # Economic indicators
        if currency in self.indicators:
            econ_score = self.indicators[currency].get_economic_score()
            score += (econ_score['overall_score'] - 50) * 0.4
            factors.append(f"Econ: {econ_score['interpretation']}")

        # Central bank stance
        if currency in self.central_banks:
            stance = self.central_banks[currency].get_policy_stance()
            if stance == 'hawkish':
                score += 15
            elif stance == 'dovish':
                score -= 15
            factors.append(f"CB: {stance}")

        # Event risk
        risk = self.calendar.currency_risk_score(currency)
        if risk['recommendation'] == 'avoid':
            factors.append("High event risk")

        return {
            'currency': currency,
            'score': max(0, min(100, score)),
            'bias': 'bullish' if score > 60 else ('bearish' if score < 40 else 'neutral'),
            'factors': factors
        }

    def rank_currencies(self) -> List[Dict]:
        """Rank all currencies by fundamental score."""
        rankings = []
        for currency in self.central_banks.keys():
            rankings.append(self.calculate_currency_score(currency))
        return sorted(rankings, key=lambda x: x['score'], reverse=True)

    def get_pair_recommendations(self) -> List[Dict]:
        """Get pair recommendations based on rankings."""
        rankings = self.rank_currencies()
        if len(rankings) < 2:
            return []

        recommendations = []
        strongest = rankings[0]
        weakest = rankings[-1]

        if strongest['score'] - weakest['score'] > 20:
            recommendations.append({
                'long': strongest['currency'],
                'short': weakest['currency'],
                'score_diff': strongest['score'] - weakest['score'],
                'confidence': 'high' if strongest['score'] - weakest['score'] > 30 else 'medium'
            })

        return recommendations

    def print_report(self) -> None:
        """Print formatted dashboard report."""
        print("=" * 50)
        print("  FUNDAMENTAL ANALYSIS DASHBOARD")
        print("=" * 50)

        print("\nCurrency Rankings:")
        for r in self.rank_currencies():
            print(f"  {r['currency']}: {r['score']:.0f} ({r['bias']})")
            print(f"    Factors: {', '.join(r['factors'])}")

        print("\nRecommendations:")
        for rec in self.get_pair_recommendations():
            print(f"  Long {rec['long']}/Short {rec['short']}")
            print(f"  Confidence: {rec['confidence']}")

Module Project: Fundamental Analysis Dashboard

Build a production-ready fundamental analysis system that integrates all concepts from this module.

class FundamentalAnalysisDashboard:
    """
    Production-ready fundamental analysis dashboard.
    
    Integrates: Economic indicators, Central banks, Calendar, Intermarket.
    Provides: Currency rankings, Pair recommendations, Risk assessment.
    """
    
    CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'CHF', 'NZD']
    
    def __init__(self):
        self.indicator_trackers: Dict[str, EconomicIndicatorTracker] = {}
        self.central_banks: Dict[str, CentralBank] = {}
        self.calendar = EconomicCalendar()
        self.intermarket = IntermarketAnalyzer()
        self.currency_scores: Dict[str, float] = {}
    
    def initialize_country(self, currency: str, country: str, 
                          current_rate: float, cb_name: str) -> None:
        """Initialize tracking for a country/currency."""
        self.indicator_trackers[currency] = EconomicIndicatorTracker(country)
        self.central_banks[currency] = CentralBank(cb_name, currency, current_rate)
    
    def update_indicator(self, currency: str, indicator: str,
                        actual: float, forecast: float, previous: float) -> None:
        """Update an economic indicator."""
        if currency in self.indicator_trackers:
            self.indicator_trackers[currency].update_indicator(
                indicator, datetime.now(), actual, forecast, previous
            )
    
    def update_rate_decision(self, currency: str, new_rate: float,
                            expected: float, tone: str = 'neutral') -> None:
        """Record a rate decision."""
        if currency in self.central_banks:
            self.central_banks[currency].add_rate_decision(
                datetime.now(), new_rate, expected, tone
            )
    
    def calculate_fundamental_score(self, currency: str) -> Dict:
        """Calculate comprehensive fundamental score."""
        score = 50  # Neutral base
        components = {}
        
        # 1. Economic Indicators (40% weight)
        if currency in self.indicator_trackers:
            econ = self.indicator_trackers[currency].get_economic_score()
            econ_score = econ['overall_score']
            score += (econ_score - 50) * 0.4
            components['economic'] = econ_score
        
        # 2. Central Bank Stance (30% weight)
        if currency in self.central_banks:
            cb = self.central_banks[currency]
            stance = cb.get_policy_stance()
            stance_score = 70 if stance == 'hawkish' else (30 if stance == 'dovish' else 50)
            score += (stance_score - 50) * 0.3
            components['central_bank'] = {
                'rate': cb.current_rate,
                'stance': stance,
                'score': stance_score
            }
        
        # 3. Rate Differentials (20% weight)
        if len(self.central_banks) > 1:
            avg_rate = np.mean([cb.current_rate for cb in self.central_banks.values()])
            if currency in self.central_banks:
                diff = self.central_banks[currency].current_rate - avg_rate
                diff_score = 50 + diff * 10  # +10 points per 1% above average
                score += (diff_score - 50) * 0.2
                components['rate_differential'] = diff
        
        # 4. Event Risk (10% weight - penalty only)
        risk = self.calendar.currency_risk_score(currency)
        if risk['risk_score'] >= 5:
            score -= 5
            components['event_risk'] = 'high'
        else:
            components['event_risk'] = 'normal'
        
        final_score = max(0, min(100, score))
        self.currency_scores[currency] = final_score
        
        return {
            'currency': currency,
            'score': final_score,
            'bias': self._score_to_bias(final_score),
            'components': components
        }
    
    def _score_to_bias(self, score: float) -> str:
        """Convert score to bias label."""
        if score >= 65:
            return 'strong_bullish'
        elif score >= 55:
            return 'bullish'
        elif score >= 45:
            return 'neutral'
        elif score >= 35:
            return 'bearish'
        return 'strong_bearish'
    
    def get_currency_rankings(self) -> List[Dict]:
        """Rank all currencies by fundamental score."""
        rankings = []
        for currency in self.central_banks.keys():
            rankings.append(self.calculate_fundamental_score(currency))
        return sorted(rankings, key=lambda x: x['score'], reverse=True)
    
    def get_trade_recommendations(self) -> List[Dict]:
        """Generate trade recommendations."""
        rankings = self.get_currency_rankings()
        recommendations = []
        
        if len(rankings) < 2:
            return recommendations
        
        # Top recommendation: strongest vs weakest
        strongest = rankings[0]
        weakest = rankings[-1]
        
        score_diff = strongest['score'] - weakest['score']
        if score_diff >= 15:
            confidence = 'high' if score_diff >= 30 else ('medium' if score_diff >= 20 else 'low')
            recommendations.append({
                'type': 'divergence',
                'long': strongest['currency'],
                'short': weakest['currency'],
                'long_score': strongest['score'],
                'short_score': weakest['score'],
                'score_differential': score_diff,
                'confidence': confidence,
                'rationale': f"{strongest['currency']} {strongest['bias']} vs {weakest['currency']} {weakest['bias']}"
            })
        
        # Secondary: second strongest vs second weakest
        if len(rankings) >= 4:
            second_strong = rankings[1]
            second_weak = rankings[-2]
            diff2 = second_strong['score'] - second_weak['score']
            
            if diff2 >= 15:
                recommendations.append({
                    'type': 'secondary',
                    'long': second_strong['currency'],
                    'short': second_weak['currency'],
                    'long_score': second_strong['score'],
                    'short_score': second_weak['score'],
                    'score_differential': diff2,
                    'confidence': 'medium' if diff2 >= 20 else 'low',
                    'rationale': f"Secondary divergence play"
                })
        
        return recommendations
    
    def print_dashboard(self) -> None:
        """Print formatted dashboard."""
        print("\n" + "=" * 60)
        print("  FUNDAMENTAL ANALYSIS DASHBOARD")
        print(f"  {datetime.now().strftime('%Y-%m-%d %H:%M')}")
        print("=" * 60)
        
        # Currency Rankings
        print("\n" + "-" * 40)
        print("CURRENCY RANKINGS")
        print("-" * 40)
        
        for rank, data in enumerate(self.get_currency_rankings(), 1):
            bar = '█' * int(data['score'] / 10)
            print(f"{rank}. {data['currency']}: {data['score']:.0f} {bar}")
            print(f"   Bias: {data['bias']}")
            if 'central_bank' in data['components']:
                cb = data['components']['central_bank']
                print(f"   Rate: {cb['rate']:.2f}% | Stance: {cb['stance']}")
        
        # Trade Recommendations
        print("\n" + "-" * 40)
        print("TRADE RECOMMENDATIONS")
        print("-" * 40)
        
        recommendations = self.get_trade_recommendations()
        if recommendations:
            for i, rec in enumerate(recommendations, 1):
                print(f"\n{i}. Long {rec['long']} / Short {rec['short']}")
                print(f"   Scores: {rec['long_score']:.0f} vs {rec['short_score']:.0f}")
                print(f"   Differential: {rec['score_differential']:.0f} points")
                print(f"   Confidence: {rec['confidence'].upper()}")
                print(f"   Rationale: {rec['rationale']}")
        else:
            print("\nNo clear opportunities - markets balanced")
        
        # Upcoming Events
        print("\n" + "-" * 40)
        print("HIGH-IMPACT EVENTS (24H)")
        print("-" * 40)
        
        high_impact = self.calendar.get_upcoming(24, EventImpact.HIGH)
        if high_impact:
            for event in high_impact[:5]:
                print(f"  {event.datetime.strftime('%H:%M')} - {event.country}: {event.name}")
        else:
            print("  No high-impact events scheduled")
        
        print("\n" + "=" * 60)
# Demo the dashboard
dashboard = FundamentalAnalysisDashboard()

# Initialize major currencies
dashboard.initialize_country('USD', 'US', 5.25, 'Federal Reserve')
dashboard.initialize_country('EUR', 'EU', 4.50, 'ECB')
dashboard.initialize_country('GBP', 'UK', 5.00, 'Bank of England')
dashboard.initialize_country('JPY', 'JP', 0.10, 'Bank of Japan')
dashboard.initialize_country('AUD', 'AU', 4.35, 'RBA')
dashboard.initialize_country('CAD', 'CA', 5.00, 'Bank of Canada')

# Add some indicator data
dashboard.update_indicator('USD', 'Non-Farm Payrolls', 303, 212, 275)
dashboard.update_indicator('USD', 'CPI YoY', 3.5, 3.4, 3.2)
dashboard.update_indicator('EUR', 'CPI YoY', 2.4, 2.5, 2.6)
dashboard.update_indicator('GBP', 'CPI YoY', 3.2, 3.4, 4.0)
dashboard.update_indicator('JPY', 'CPI YoY', 2.8, 2.6, 2.5)

# Add rate decisions
dashboard.update_rate_decision('USD', 5.25, 5.25, 'hawkish')
dashboard.update_rate_decision('EUR', 4.50, 4.50, 'dovish')
dashboard.update_rate_decision('JPY', 0.10, 0.10, 'dovish')

# Add calendar events
base_time = datetime.now() + timedelta(hours=2)
dashboard.calendar.add_event(CalendarEvent(
    'FOMC Minutes', 'US', base_time, EventImpact.HIGH
))
dashboard.calendar.add_event(CalendarEvent(
    'ECB Speech', 'EU', base_time + timedelta(hours=3), EventImpact.MEDIUM
))

# Print the dashboard
dashboard.print_dashboard()

Key Takeaways

  • Economic Indicators: Track GDP, inflation, employment to gauge economic health and currency direction
  • Central Banks: Monitor rate decisions and policy stance (hawkish vs dovish) for major currency moves
  • Rate Differentials: Higher interest rates attract capital, supporting currency strength
  • Economic Calendar: Schedule trading around high-impact events; avoid unexpected volatility
  • Intermarket Analysis: Currencies correlate with commodities (AUD-Gold, CAD-Oil) and bonds
  • Carry Trade: Long high-yield, short low-yield currencies when conditions favor risk-on
  • Divergence: Policy divergence between central banks creates trending opportunities
  • Combine Analysis: Best results come from aligning fundamental bias with technical setups

Next: Module 7 - Commodity Trading where we'll explore gold, oil, and agricultural markets with their currency relationships.

Module 7: Commodity Trading

Part 2: Analysis & Strategies

Duration Exercises
~2.5 hours 6

Learning Objectives

  • Understand gold market dynamics and safe-haven trading
  • Analyze crude oil supply/demand and inventory data
  • Trade agricultural commodities with seasonal patterns
  • Exploit commodity currency correlations (AUD, CAD, NOK)

Prerequisites

  • Modules 1-6 (Forex/Futures fundamentals, technical & fundamental analysis)
  • Understanding of correlation analysis
  • Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')

7.1 Gold Trading

Gold (XAU) is the premier safe-haven asset, with unique market dynamics driven by fear, inflation expectations, and USD strength.

Gold Market Fundamentals

Factor Impact on Gold Explanation
USD Strength Negative Gold priced in USD; strong USD = cheaper gold
Real Interest Rates Negative Higher real yields = opportunity cost of holding gold
Inflation Expectations Positive Gold as inflation hedge
Geopolitical Risk Positive Safe-haven demand
Central Bank Buying Positive Reserve diversification
class GoldAnalyzer:
    """Analyze gold market dynamics and generate trading signals."""
    
    def __init__(self):
        self.gold_prices: pd.Series = pd.Series(dtype=float)
        self.dxy_prices: pd.Series = pd.Series()  # USD Index
        self.real_yields: pd.Series = pd.Series()  # 10Y TIPS yield
        self.vix: pd.Series = pd.Series()  # Fear index
    
    def set_data(self, gold: pd.Series, dxy: pd.Series = None,
                 real_yields: pd.Series = None, vix: pd.Series = None) -> None:
        """Set market data."""
        self.gold_prices = gold
        if dxy is not None:
            self.dxy_prices = dxy
        if real_yields is not None:
            self.real_yields = real_yields
        if vix is not None:
            self.vix = vix
    
    def calculate_gold_usd_correlation(self, window: int = 20) -> pd.Series:
        """Calculate rolling correlation between gold and USD."""
        if self.dxy_prices.empty:
            return pd.Series(dtype=float)
        
        gold_ret = self.gold_prices.pct_change()
        dxy_ret = self.dxy_prices.pct_change()
        return gold_ret.rolling(window).corr(dxy_ret)
    
    def safe_haven_signal(self, vix_threshold: float = 25) -> Dict:
        """Generate safe-haven demand signal."""
        if self.vix.empty:
            return {'signal': 'no_data'}
        
        current_vix = self.vix.iloc[-1]
        vix_change = self.vix.iloc[-1] - self.vix.iloc[-5] if len(self.vix) >= 5 else 0
        
        if current_vix > vix_threshold and vix_change > 5:
            signal = 'strong_buy'
            reason = 'Fear spike - safe haven demand'
        elif current_vix > vix_threshold:
            signal = 'buy'
            reason = 'Elevated fear - gold supportive'
        elif current_vix < 15 and vix_change < -3:
            signal = 'sell'
            reason = 'Risk-on environment - gold headwind'
        else:
            signal = 'neutral'
            reason = 'Normal volatility'
        
        return {
            'signal': signal,
            'vix': current_vix,
            'vix_change': vix_change,
            'reason': reason
        }
    
    def real_yield_signal(self) -> Dict:
        """Generate signal based on real yields."""
        if self.real_yields.empty:
            return {'signal': 'no_data'}
        
        current_yield = self.real_yields.iloc[-1]
        yield_ma = self.real_yields.rolling(20).mean().iloc[-1]
        
        # Gold is negative correlated with real yields
        if current_yield < yield_ma and current_yield < 0:
            signal = 'buy'
            reason = 'Negative real yields support gold'
        elif current_yield > yield_ma and current_yield > 1.5:
            signal = 'sell'
            reason = 'High real yields headwind for gold'
        else:
            signal = 'neutral'
            reason = 'Real yields neutral'
        
        return {
            'signal': signal,
            'real_yield': current_yield,
            'yield_ma': yield_ma,
            'reason': reason
        }
    
    def combined_signal(self) -> Dict:
        """Combine all factors for overall gold signal."""
        signals = {
            'safe_haven': self.safe_haven_signal(),
            'real_yield': self.real_yield_signal()
        }
        
        # Score each signal
        score = 0
        signal_map = {'strong_buy': 2, 'buy': 1, 'neutral': 0, 'sell': -1, 'strong_sell': -2}
        
        for name, sig in signals.items():
            if sig['signal'] in signal_map:
                score += signal_map[sig['signal']]
        
        if score >= 2:
            overall = 'strong_buy'
        elif score >= 1:
            overall = 'buy'
        elif score <= -2:
            overall = 'strong_sell'
        elif score <= -1:
            overall = 'sell'
        else:
            overall = 'neutral'
        
        return {
            'overall': overall,
            'score': score,
            'components': signals
        }

# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')

gold_analyzer = GoldAnalyzer()
gold_analyzer.set_data(
    gold=pd.Series(2000 + np.cumsum(np.random.normal(0, 15, 60)), index=dates),
    dxy=pd.Series(104 + np.cumsum(np.random.normal(0, 0.3, 60)), index=dates),
    vix=pd.Series(np.clip(18 + np.cumsum(np.random.normal(0, 1, 60)), 10, 40), index=dates),
    real_yields=pd.Series(0.5 + np.cumsum(np.random.normal(0, 0.05, 60)), index=dates)
)

print("Gold Analysis:")
print(f"\nSafe Haven Signal: {gold_analyzer.safe_haven_signal()}")
print(f"\nReal Yield Signal: {gold_analyzer.real_yield_signal()}")
print(f"\nCombined Signal: {gold_analyzer.combined_signal()['overall']}")

7.2 Oil Trading

Crude oil (WTI/Brent) is driven by supply/demand dynamics, OPEC decisions, and inventory data.

Key Oil Market Drivers

Factor Impact Data Source
EIA Inventory Build = Bearish, Draw = Bullish Weekly (Wed)
OPEC Production Cuts = Bullish, Increases = Bearish Monthly
Global Demand GDP growth correlation IMF, EIA
Geopolitical Supply disruption = Bullish News
USD Strength Negative correlation DXY
class OilAnalyzer:
    """Analyze crude oil market and inventory data."""
    
    def __init__(self):
        self.prices: pd.Series = pd.Series(dtype=float)
        self.inventory_data: List[Dict] = []
        self.opec_production: List[Dict] = []
    
    def set_prices(self, prices: pd.Series) -> None:
        """Set oil price data."""
        self.prices = prices
    
    def add_inventory_report(self, date: datetime, actual: float,
                            forecast: float, previous: float) -> None:
        """Add EIA inventory report (in millions of barrels)."""
        change = actual - previous
        surprise = actual - forecast
        
        self.inventory_data.append({
            'date': date,
            'actual': actual,
            'forecast': forecast,
            'previous': previous,
            'change': change,
            'surprise': surprise,
            'type': 'build' if change > 0 else 'draw'
        })
    
    def inventory_trend(self, periods: int = 4) -> Dict:
        """Analyze recent inventory trend."""
        if len(self.inventory_data) < periods:
            return {'trend': 'insufficient_data'}
        
        recent = self.inventory_data[-periods:]
        builds = sum(1 for r in recent if r['type'] == 'build')
        draws = periods - builds
        
        total_change = sum(r['change'] for r in recent)
        avg_surprise = np.mean([r['surprise'] for r in recent])
        
        if draws > builds:
            trend = 'bullish'
        elif builds > draws:
            trend = 'bearish'
        else:
            trend = 'neutral'
        
        return {
            'trend': trend,
            'builds': builds,
            'draws': draws,
            'total_change_mb': total_change,
            'avg_surprise': avg_surprise
        }
    
    def inventory_signal(self) -> Dict:
        """Generate trading signal from latest inventory."""
        if not self.inventory_data:
            return {'signal': 'no_data'}
        
        latest = self.inventory_data[-1]
        trend = self.inventory_trend()
        
        # Signal based on surprise and trend
        if latest['surprise'] < -3 and trend['trend'] == 'bullish':
            signal = 'strong_buy'
            reason = 'Large draw surprise + bullish trend'
        elif latest['surprise'] < -1:
            signal = 'buy'
            reason = 'Draw larger than expected'
        elif latest['surprise'] > 3 and trend['trend'] == 'bearish':
            signal = 'strong_sell'
            reason = 'Large build surprise + bearish trend'
        elif latest['surprise'] > 1:
            signal = 'sell'
            reason = 'Build larger than expected'
        else:
            signal = 'neutral'
            reason = 'Inventory in line with expectations'
        
        return {
            'signal': signal,
            'reason': reason,
            'latest_change': latest['change'],
            'surprise': latest['surprise'],
            'trend': trend['trend']
        }
    
    def calculate_seasonality(self) -> Dict:
        """Calculate typical seasonal patterns."""
        if self.prices.empty:
            return {}
        
        # Group by month and calculate average returns
        returns = self.prices.pct_change()
        monthly_returns = returns.groupby(returns.index.month).mean() * 100
        
        return {
            'monthly_avg_returns': monthly_returns.to_dict(),
            'best_month': monthly_returns.idxmax(),
            'worst_month': monthly_returns.idxmin()
        }

# Demo
oil_analyzer = OilAnalyzer()
oil_analyzer.set_prices(pd.Series(75 + np.cumsum(np.random.normal(0, 1, 60)), index=dates))

# Add inventory reports
oil_analyzer.add_inventory_report(datetime(2024, 4, 3), 451.2, 453.0, 452.5)  # Draw
oil_analyzer.add_inventory_report(datetime(2024, 4, 10), 449.8, 451.0, 451.2)  # Draw
oil_analyzer.add_inventory_report(datetime(2024, 4, 17), 448.5, 450.5, 449.8)  # Draw
oil_analyzer.add_inventory_report(datetime(2024, 4, 24), 446.2, 449.0, 448.5)  # Large draw

print("Oil Inventory Analysis:")
print(f"\nTrend: {oil_analyzer.inventory_trend()}")
print(f"\nSignal: {oil_analyzer.inventory_signal()}")

7.3 Agricultural Commodities

Agricultural commodities (grains, softs) have strong seasonal patterns driven by planting, growing, and harvest cycles.

Major Agricultural Markets

Commodity Symbol Key Factors
Corn ZC US planting (Apr-May), Harvest (Sep-Nov)
Wheat ZW Winter/Spring varieties, Global weather
Soybeans ZS China demand, Brazil crop
Coffee KC Brazil frost, Colombian production
Sugar SB Brazil production, Ethanol demand
class AgriculturalAnalyzer:
    """Analyze agricultural commodities with seasonal patterns."""
    
    # Typical seasonal patterns (month -> expected direction)
    SEASONAL_PATTERNS = {
        'corn': {
            1: 'neutral', 2: 'neutral', 3: 'bullish',  # Pre-planting
            4: 'bullish', 5: 'bullish', 6: 'bearish',  # Planting uncertainty
            7: 'volatile', 8: 'volatile', 9: 'bearish',  # Weather/harvest
            10: 'bearish', 11: 'bearish', 12: 'neutral'  # Harvest pressure
        },
        'wheat': {
            1: 'neutral', 2: 'neutral', 3: 'bullish',
            4: 'bullish', 5: 'volatile', 6: 'bearish',
            7: 'bearish', 8: 'neutral', 9: 'neutral',
            10: 'neutral', 11: 'neutral', 12: 'neutral'
        },
        'soybeans': {
            1: 'neutral', 2: 'bullish', 3: 'bullish',
            4: 'bullish', 5: 'volatile', 6: 'volatile',
            7: 'volatile', 8: 'bearish', 9: 'bearish',
            10: 'bearish', 11: 'neutral', 12: 'neutral'
        }
    }
    
    def __init__(self, commodity: str):
        self.commodity = commodity.lower()
        self.prices: pd.Series = pd.Series(dtype=float)
        self.weather_events: List[Dict] = []
    
    def set_prices(self, prices: pd.Series) -> None:
        """Set price data."""
        self.prices = prices
    
    def add_weather_event(self, date: datetime, event_type: str,
                         region: str, severity: str) -> None:
        """Record weather event affecting crop."""
        impact_map = {
            ('drought', 'severe'): 'very_bullish',
            ('drought', 'moderate'): 'bullish',
            ('frost', 'severe'): 'very_bullish',
            ('frost', 'moderate'): 'bullish',
            ('flooding', 'severe'): 'bullish',
            ('ideal', 'any'): 'bearish'
        }
        
        impact = impact_map.get((event_type, severity), 'neutral')
        
        self.weather_events.append({
            'date': date,
            'event': event_type,
            'region': region,
            'severity': severity,
            'impact': impact
        })
    
    def get_seasonal_bias(self, month: int = None) -> Dict:
        """Get seasonal bias for current or specified month."""
        if month is None:
            month = datetime.now().month
        
        pattern = self.SEASONAL_PATTERNS.get(self.commodity, {})
        bias = pattern.get(month, 'unknown')
        
        return {
            'commodity': self.commodity,
            'month': month,
            'seasonal_bias': bias,
            'explanation': self._get_seasonal_explanation(month)
        }
    
    def _get_seasonal_explanation(self, month: int) -> str:
        """Get explanation for seasonal pattern."""
        explanations = {
            'corn': {
                (3, 5): 'Pre-planting uncertainty typically bullish',
                (6, 8): 'Weather-driven volatility during growing season',
                (9, 11): 'Harvest pressure typically bearish'
            },
            'soybeans': {
                (2, 5): 'South American harvest, US planting',
                (6, 8): 'Critical growing period',
                (9, 11): 'US harvest pressure'
            }
        }
        
        commodity_exp = explanations.get(self.commodity, {})
        for (start, end), exp in commodity_exp.items():
            if start <= month <= end:
                return exp
        return 'Standard seasonal period'
    
    def calculate_historical_seasonality(self) -> pd.DataFrame:
        """Calculate historical seasonal performance."""
        if self.prices.empty:
            return pd.DataFrame()
        
        returns = self.prices.pct_change() * 100
        monthly = returns.groupby(returns.index.month).agg(['mean', 'std', 'count'])
        monthly.columns = ['avg_return', 'volatility', 'observations']
        monthly['win_rate'] = returns.groupby(returns.index.month).apply(
            lambda x: (x > 0).sum() / len(x) * 100 if len(x) > 0 else 0
        )
        
        return monthly
    
    def weather_signal(self) -> Dict:
        """Generate signal from recent weather events."""
        if not self.weather_events:
            return {'signal': 'neutral', 'reason': 'No weather events'}
        
        # Look at events in last 30 days
        cutoff = datetime.now() - timedelta(days=30)
        recent = [e for e in self.weather_events if e['date'] > cutoff]
        
        if not recent:
            return {'signal': 'neutral', 'reason': 'No recent weather events'}
        
        # Score events
        impact_scores = {'very_bullish': 2, 'bullish': 1, 'neutral': 0, 'bearish': -1}
        total_score = sum(impact_scores.get(e['impact'], 0) for e in recent)
        
        if total_score >= 2:
            signal = 'buy'
        elif total_score <= -1:
            signal = 'sell'
        else:
            signal = 'neutral'
        
        return {
            'signal': signal,
            'events': len(recent),
            'score': total_score,
            'recent_events': recent[-3:]
        }

# Demo
corn_analyzer = AgriculturalAnalyzer('corn')
corn_analyzer.set_prices(pd.Series(450 + np.cumsum(np.random.normal(0, 5, 60)), index=dates))

# Add weather events
corn_analyzer.add_weather_event(datetime.now() - timedelta(days=5), 'drought', 'US Midwest', 'moderate')

print(f"Corn Seasonal Bias (April): {corn_analyzer.get_seasonal_bias(4)}")
print(f"\nWeather Signal: {corn_analyzer.weather_signal()}")

7.4 Commodity Currencies

Commodity currencies (AUD, CAD, NOK) are strongly correlated with their primary export commodities.

Key Commodity Currency Relationships

Currency Primary Commodity Correlation Notes
AUD Gold, Iron Ore Positive China demand key
CAD Crude Oil Positive Oil sands production
NOK Brent Crude Positive North Sea production
NZD Dairy Positive Fonterra prices
ZAR Gold, Platinum Positive Mining sector
class CommodityCurrencyAnalyzer:
    """Analyze relationships between commodities and currencies."""
    
    RELATIONSHIPS = {
        'AUD': {'commodities': ['gold', 'iron_ore'], 'expected_corr': 0.6},
        'CAD': {'commodities': ['oil'], 'expected_corr': 0.5},
        'NOK': {'commodities': ['brent'], 'expected_corr': 0.6},
        'NZD': {'commodities': ['dairy'], 'expected_corr': 0.4},
        'ZAR': {'commodities': ['gold', 'platinum'], 'expected_corr': 0.5}
    }
    
    def __init__(self):
        self.currency_data: Dict[str, pd.Series] = {}
        self.commodity_data: Dict[str, pd.Series] = {}
    
    def add_currency(self, currency: str, prices: pd.Series) -> None:
        """Add currency pair data."""
        self.currency_data[currency] = prices
    
    def add_commodity(self, commodity: str, prices: pd.Series) -> None:
        """Add commodity price data."""
        self.commodity_data[commodity] = prices
    
    def calculate_correlation(self, currency: str, commodity: str,
                             window: int = 20) -> float:
        """Calculate rolling correlation."""
        if currency not in self.currency_data or commodity not in self.commodity_data:
            return 0.0
        
        curr_ret = self.currency_data[currency].pct_change()
        comm_ret = self.commodity_data[commodity].pct_change()
        
        aligned = pd.concat([curr_ret, comm_ret], axis=1).dropna()
        if len(aligned) < window:
            return 0.0
        
        return aligned.iloc[-window:, 0].corr(aligned.iloc[-window:, 1])
    
    def check_divergence(self, currency: str) -> Dict:
        """Check for divergence between currency and its commodities."""
        if currency not in self.RELATIONSHIPS:
            return {'currency': currency, 'divergence': False}
        
        rel = self.RELATIONSHIPS[currency]
        expected = rel['expected_corr']
        
        correlations = []
        for comm in rel['commodities']:
            if comm in self.commodity_data:
                corr = self.calculate_correlation(currency, comm)
                correlations.append({'commodity': comm, 'correlation': corr})
        
        if not correlations:
            return {'currency': currency, 'divergence': False, 'reason': 'No commodity data'}
        
        avg_corr = np.mean([c['correlation'] for c in correlations])
        divergence = avg_corr < expected * 0.5 or avg_corr < 0
        
        return {
            'currency': currency,
            'expected_correlation': expected,
            'actual_correlation': avg_corr,
            'divergence': divergence,
            'signal': 'divergence_trade' if divergence else 'follow_commodity',
            'details': correlations
        }
    
    def commodity_currency_signal(self, currency: str) -> Dict:
        """Generate trading signal based on commodity-currency relationship."""
        if currency not in self.RELATIONSHIPS:
            return {'signal': 'no_relationship'}
        
        rel = self.RELATIONSHIPS[currency]
        signals = []
        
        for comm in rel['commodities']:
            if comm not in self.commodity_data:
                continue
            
            comm_prices = self.commodity_data[comm]
            comm_ret_20d = (comm_prices.iloc[-1] / comm_prices.iloc[-20] - 1) * 100 if len(comm_prices) >= 20 else 0
            
            if comm_ret_20d > 5:
                signals.append({'commodity': comm, 'signal': 'bullish', 'return': comm_ret_20d})
            elif comm_ret_20d < -5:
                signals.append({'commodity': comm, 'signal': 'bearish', 'return': comm_ret_20d})
            else:
                signals.append({'commodity': comm, 'signal': 'neutral', 'return': comm_ret_20d})
        
        if not signals:
            return {'signal': 'no_data'}
        
        bullish = sum(1 for s in signals if s['signal'] == 'bullish')
        bearish = sum(1 for s in signals if s['signal'] == 'bearish')
        
        if bullish > bearish:
            overall = 'buy_currency'
        elif bearish > bullish:
            overall = 'sell_currency'
        else:
            overall = 'neutral'
        
        return {
            'currency': currency,
            'signal': overall,
            'commodity_signals': signals
        }

# Demo
cc_analyzer = CommodityCurrencyAnalyzer()

# Add data
cc_analyzer.add_currency('AUD', pd.Series(0.65 + np.cumsum(np.random.normal(0, 0.002, 60)), index=dates))
cc_analyzer.add_currency('CAD', pd.Series(1.35 + np.cumsum(np.random.normal(0, 0.002, 60)), index=dates))
cc_analyzer.add_commodity('gold', pd.Series(2000 + np.cumsum(np.random.normal(2, 15, 60)), index=dates))
cc_analyzer.add_commodity('oil', pd.Series(75 + np.cumsum(np.random.normal(0.5, 1, 60)), index=dates))

print("AUD-Gold Divergence Check:")
print(cc_analyzer.check_divergence('AUD'))

print("\nCAD Trading Signal:")
print(cc_analyzer.commodity_currency_signal('CAD'))

Exercises

Exercise 1: Gold Safe Haven Analyzer (Guided)

Complete the SafeHavenAnalyzer class that tracks gold's safe-haven behavior.

class SafeHavenAnalyzer:
    """Analyze gold's safe-haven characteristics."""
    
    def __init__(self):
        self.gold_prices: pd.Series = pd.Series(dtype=float)
        self.spx_prices: pd.Series = pd.Series(dtype=float)
        self.vix_prices: pd.Series = pd.Series(dtype=float)
    
    def set_data(self, gold: pd.Series, spx: pd.Series, vix: pd.Series) -> None:
        """Set market data."""
        self.gold_prices = gold
        self.spx_prices = spx
        self.vix_prices = vix
    
    def calculate_crisis_correlation(self, vix_threshold: float = 25) -> float:
        """Calculate gold-SPX correlation during high VIX periods."""
        gold_ret = self.gold_prices.pct_change()
        spx_ret = self.spx_prices.pct_change()
        
        # Filter for crisis periods
        crisis_mask = self.vix_prices ______ vix_threshold
        
        crisis_gold = gold_ret[crisis_mask]
        crisis_spx = spx_ret[crisis_mask]
        
        if len(crisis_gold) < 5:
            return 0.0
        
        return crisis_gold.______(crisis_spx)
    
    def safe_haven_score(self) -> Dict:
        """Calculate safe-haven effectiveness score."""
        crisis_corr = self.calculate_crisis_correlation()
        
        # Good safe haven has negative correlation during crisis
        if crisis_corr < -0.3:
            score = 100
            rating = 'excellent'
        elif crisis_corr < 0:
            score = 70
            rating = 'good'
        elif crisis_corr < 0.3:
            score = ______
            rating = 'moderate'
        else:
            score = 20
            rating = 'poor'
        
        return {
            'crisis_correlation': crisis_corr,
            'safe_haven_score': score,
            'rating': rating
        }

# Test
sha = SafeHavenAnalyzer()
sha.set_data(
    gold=pd.Series(2000 + np.cumsum(np.random.normal(1, 10, 100)), index=pd.date_range('2024-01-01', periods=100)),
    spx=pd.Series(5000 + np.cumsum(np.random.normal(0, 20, 100)), index=pd.date_range('2024-01-01', periods=100)),
    vix=pd.Series(np.clip(18 + np.cumsum(np.random.normal(0, 2, 100)), 10, 50), index=pd.date_range('2024-01-01', periods=100))
)
print(f"Safe Haven Analysis: {sha.safe_haven_score()}")
Solution 1
class SafeHavenAnalyzer:
    def __init__(self):
        self.gold_prices: pd.Series = pd.Series(dtype=float)
        self.spx_prices: pd.Series = pd.Series(dtype=float)
        self.vix_prices: pd.Series = pd.Series(dtype=float)

    def set_data(self, gold: pd.Series, spx: pd.Series, vix: pd.Series) -> None:
        self.gold_prices = gold
        self.spx_prices = spx
        self.vix_prices = vix

    def calculate_crisis_correlation(self, vix_threshold: float = 25) -> float:
        gold_ret = self.gold_prices.pct_change()
        spx_ret = self.spx_prices.pct_change()

        crisis_mask = self.vix_prices > vix_threshold

        crisis_gold = gold_ret[crisis_mask]
        crisis_spx = spx_ret[crisis_mask]

        if len(crisis_gold) < 5:
            return 0.0

        return crisis_gold.corr(crisis_spx)

    def safe_haven_score(self) -> Dict:
        crisis_corr = self.calculate_crisis_correlation()

        if crisis_corr < -0.3:
            score = 100
            rating = 'excellent'
        elif crisis_corr < 0:
            score = 70
            rating = 'good'
        elif crisis_corr < 0.3:
            score = 40
            rating = 'moderate'
        else:
            score = 20
            rating = 'poor'

        return {
            'crisis_correlation': crisis_corr,
            'safe_haven_score': score,
            'rating': rating
        }

Exercise 2: Oil Inventory Tracker (Guided)

Complete the InventoryTracker class that analyzes EIA oil inventory data.

class InventoryTracker:
    """Track and analyze oil inventory data."""
    
    def __init__(self):
        self.reports: List[Dict] = []
    
    def add_report(self, date: datetime, crude: float, gasoline: float,
                   distillate: float) -> None:
        """Add weekly inventory report (values in millions of barrels)."""
        self.reports.append({
            'date': date,
            'crude': crude,
            'gasoline': gasoline,
            'distillate': distillate,
            'total': crude + gasoline + distillate
        })
    
    def get_weekly_change(self) -> Dict:
        """Calculate week-over-week inventory changes."""
        if len(self.reports) < 2:
            return {}
        
        latest = self.reports[-1]
        previous = self.reports[______]
        
        return {
            'crude_change': latest['crude'] - previous['crude'],
            'gasoline_change': latest['gasoline'] - previous['gasoline'],
            'distillate_change': latest['distillate'] - previous['distillate'],
            'total_change': latest['total'] - previous['total']
        }
    
    def get_trend(self, weeks: int = 4) -> str:
        """Determine inventory trend over N weeks."""
        if len(self.reports) < weeks + 1:
            return 'insufficient_data'
        
        builds = 0
        for i in range(1, weeks + 1):
            if self.reports[-i]['total'] > self.reports[-i-1]['______']:
                builds += 1
        
        if builds >= weeks - 1:
            return 'building'
        elif builds <= 1:
            return 'drawing'
        return 'mixed'
    
    def generate_signal(self) -> Dict:
        """Generate trading signal from inventory data."""
        change = self.get_weekly_change()
        trend = self.get_trend()
        
        if not change:
            return {'signal': 'no_data'}
        
        total_change = change['total_change']
        
        if total_change < -5 and trend == 'drawing':
            signal = 'strong_buy'
        elif total_change < 0:
            signal = 'buy'
        elif total_change > 5 and trend == 'building':
            signal = 'strong_sell'
        elif total_change > 0:
            signal = 'sell'
        else:
            signal = 'neutral'
        
        return {'signal': signal, 'weekly_change': total_change, 'trend': trend}

# Test
tracker = InventoryTracker()
tracker.add_report(datetime(2024, 4, 3), 450, 230, 120)
tracker.add_report(datetime(2024, 4, 10), 448, 228, 118)
tracker.add_report(datetime(2024, 4, 17), 445, 225, 117)
tracker.add_report(datetime(2024, 4, 24), 442, 223, 115)
tracker.add_report(datetime(2024, 5, 1), 438, 220, 113)

print(f"Weekly Change: {tracker.get_weekly_change()}")
print(f"Trend: {tracker.get_trend()}")
print(f"Signal: {tracker.generate_signal()}")
Solution 2
class InventoryTracker:
    def __init__(self):
        self.reports: List[Dict] = []

    def add_report(self, date: datetime, crude: float, gasoline: float,
                   distillate: float) -> None:
        self.reports.append({
            'date': date,
            'crude': crude,
            'gasoline': gasoline,
            'distillate': distillate,
            'total': crude + gasoline + distillate
        })

    def get_weekly_change(self) -> Dict:
        if len(self.reports) < 2:
            return {}

        latest = self.reports[-1]
        previous = self.reports[-2]

        return {
            'crude_change': latest['crude'] - previous['crude'],
            'gasoline_change': latest['gasoline'] - previous['gasoline'],
            'distillate_change': latest['distillate'] - previous['distillate'],
            'total_change': latest['total'] - previous['total']
        }

    def get_trend(self, weeks: int = 4) -> str:
        if len(self.reports) < weeks + 1:
            return 'insufficient_data'

        builds = 0
        for i in range(1, weeks + 1):
            if self.reports[-i]['total'] > self.reports[-i-1]['total']:
                builds += 1

        if builds >= weeks - 1:
            return 'building'
        elif builds <= 1:
            return 'drawing'
        return 'mixed'

Exercise 3: Seasonal Pattern Detector (Guided)

Complete the SeasonalDetector class that identifies seasonal trading opportunities.

class SeasonalDetector:
    """Detect and trade seasonal patterns in commodities."""
    
    def __init__(self):
        self.prices: pd.Series = pd.Series(dtype=float)
    
    def set_prices(self, prices: pd.Series) -> None:
        """Set historical price data."""
        self.prices = prices
    
    def calculate_monthly_stats(self) -> pd.DataFrame:
        """Calculate statistics for each month."""
        returns = self.prices.pct_change() * 100
        
        monthly = returns.groupby(returns.index.______).agg([
            ('avg_return', 'mean'),
            ('volatility', 'std'),
            ('win_rate', lambda x: (x > 0).sum() / len(x) * 100)
        ])
        
        return monthly
    
    def get_best_months(self, top_n: int = 3) -> List[int]:
        """Get months with best historical performance."""
        stats = self.calculate_monthly_stats()
        return stats.nlargest(top_n, 'avg_return').______.tolist()
    
    def get_worst_months(self, bottom_n: int = 3) -> List[int]:
        """Get months with worst historical performance."""
        stats = self.calculate_monthly_stats()
        return stats.nsmallest(bottom_n, 'avg_return').index.tolist()
    
    def current_month_outlook(self) -> Dict:
        """Get outlook for current month based on seasonality."""
        current_month = datetime.now().______
        stats = self.calculate_monthly_stats()
        
        if current_month not in stats.index:
            return {'outlook': 'no_data'}
        
        month_stats = stats.loc[current_month]
        
        if month_stats['avg_return'] > 1 and month_stats['win_rate'] > 60:
            outlook = 'bullish'
        elif month_stats['avg_return'] < -1 and month_stats['win_rate'] < 40:
            outlook = 'bearish'
        else:
            outlook = 'neutral'
        
        return {
            'month': current_month,
            'outlook': outlook,
            'avg_return': month_stats['avg_return'],
            'win_rate': month_stats['win_rate']
        }

# Test
detector = SeasonalDetector()
# Create 2 years of simulated data
dates_2y = pd.date_range('2022-01-01', periods=500, freq='D')
detector.set_prices(pd.Series(100 + np.cumsum(np.random.normal(0.02, 1, 500)), index=dates_2y))

print(f"Best Months: {detector.get_best_months()}")
print(f"Worst Months: {detector.get_worst_months()}")
print(f"Current Outlook: {detector.current_month_outlook()}")
Solution 3
class SeasonalDetector:
    def __init__(self):
        self.prices: pd.Series = pd.Series(dtype=float)

    def set_prices(self, prices: pd.Series) -> None:
        self.prices = prices

    def calculate_monthly_stats(self) -> pd.DataFrame:
        returns = self.prices.pct_change() * 100

        monthly = returns.groupby(returns.index.month).agg([
            ('avg_return', 'mean'),
            ('volatility', 'std'),
            ('win_rate', lambda x: (x > 0).sum() / len(x) * 100)
        ])

        return monthly

    def get_best_months(self, top_n: int = 3) -> List[int]:
        stats = self.calculate_monthly_stats()
        return stats.nlargest(top_n, 'avg_return').index.tolist()

    def get_worst_months(self, bottom_n: int = 3) -> List[int]:
        stats = self.calculate_monthly_stats()
        return stats.nsmallest(bottom_n, 'avg_return').index.tolist()

    def current_month_outlook(self) -> Dict:
        current_month = datetime.now().month
        stats = self.calculate_monthly_stats()

        if current_month not in stats.index:
            return {'outlook': 'no_data'}

        month_stats = stats.loc[current_month]

        if month_stats['avg_return'] > 1 and month_stats['win_rate'] > 60:
            outlook = 'bullish'
        elif month_stats['avg_return'] < -1 and month_stats['win_rate'] < 40:
            outlook = 'bearish'
        else:
            outlook = 'neutral'

        return {
            'month': current_month,
            'outlook': outlook,
            'avg_return': month_stats['avg_return'],
            'win_rate': month_stats['win_rate']
        }

Exercise 4: Complete Gold Trading System (Open-ended)

Build a comprehensive gold trading system that: - Tracks USD correlation - Monitors real yields impact - Detects safe-haven flows - Generates actionable signals

# Exercise 4: Complete Gold Trading System (Open-ended)
#
# Requirements:
# 1. Create class GoldTradingSystem
# 2. Track: gold prices, DXY, real yields, VIX, SPX
# 3. Calculate gold-USD correlation (should be negative)
# 4. Monitor real yields impact (negative correlation with gold)
# 5. Detect safe-haven demand (VIX spikes)
# 6. Combine factors into weighted signal
#
# Your implementation:
Solution 4
class GoldTradingSystem:
    def __init__(self):
        self.gold: pd.Series = pd.Series(dtype=float)
        self.dxy: pd.Series = pd.Series(dtype=float)
        self.real_yields: pd.Series = pd.Series(dtype=float)
        self.vix: pd.Series = pd.Series(dtype=float)
        self.spx: pd.Series = pd.Series(dtype=float)

    def set_data(self, gold, dxy, real_yields, vix, spx):
        self.gold = gold
        self.dxy = dxy
        self.real_yields = real_yields
        self.vix = vix
        self.spx = spx

    def usd_signal(self) -> Dict:
        corr = self.gold.pct_change().rolling(20).corr(self.dxy.pct_change()).iloc[-1]
        dxy_trend = self.dxy.iloc[-1] / self.dxy.iloc[-20] - 1

        if dxy_trend < -0.02:  # USD weakening
            return {'signal': 'buy', 'reason': 'USD weakness'}
        elif dxy_trend > 0.02:  # USD strengthening
            return {'signal': 'sell', 'reason': 'USD strength'}
        return {'signal': 'neutral', 'reason': 'USD stable'}

    def yield_signal(self) -> Dict:
        current = self.real_yields.iloc[-1]
        ma = self.real_yields.rolling(20).mean().iloc[-1]

        if current < 0:
            return {'signal': 'buy', 'reason': 'Negative real yields'}
        elif current > ma + 0.5:
            return {'signal': 'sell', 'reason': 'Rising real yields'}
        return {'signal': 'neutral', 'reason': 'Yields stable'}

    def safe_haven_signal(self) -> Dict:
        vix_current = self.vix.iloc[-1]
        vix_change = vix_current - self.vix.iloc[-5]

        if vix_current > 25 and vix_change > 5:
            return {'signal': 'strong_buy', 'reason': 'Fear spike'}
        elif vix_current > 20:
            return {'signal': 'buy', 'reason': 'Elevated fear'}
        elif vix_current < 15:
            return {'signal': 'sell', 'reason': 'Risk-on'}
        return {'signal': 'neutral', 'reason': 'Normal vol'}

    def combined_signal(self) -> Dict:
        signals = [
            ('usd', self.usd_signal(), 0.3),
            ('yield', self.yield_signal(), 0.3),
            ('safe_haven', self.safe_haven_signal(), 0.4)
        ]

        score_map = {'strong_buy': 2, 'buy': 1, 'neutral': 0, 'sell': -1, 'strong_sell': -2}
        total = sum(score_map.get(s[1]['signal'], 0) * s[2] for s in signals)

        if total > 0.5: return {'signal': 'buy', 'score': total}
        elif total < -0.5: return {'signal': 'sell', 'score': total}
        return {'signal': 'neutral', 'score': total}

Exercise 5: Oil Supply/Demand Analyzer (Open-ended)

Build an oil market analyzer that: - Tracks EIA inventory data - Monitors OPEC production changes - Calculates supply/demand balance - Generates trading signals

# Exercise 5: Oil Supply/Demand Analyzer (Open-ended)
#
# Requirements:
# 1. Create class OilSupplyDemandAnalyzer
# 2. Track: inventories, OPEC production, global demand estimates
# 3. Calculate implied supply/demand balance
# 4. Detect inventory trends (builds vs draws)
# 5. Generate trading signals from S/D balance
#
# Your implementation:
Solution 5
class OilSupplyDemandAnalyzer:
    def __init__(self):
        self.inventories: List[Dict] = []
        self.opec_production: List[Dict] = []
        self.demand_estimates: List[Dict] = []

    def add_inventory(self, date, crude, gasoline, distillate):
        self.inventories.append({
            'date': date,
            'crude': crude,
            'gasoline': gasoline,
            'distillate': distillate,
            'total': crude + gasoline + distillate
        })

    def add_opec_data(self, date, production, quota):
        self.opec_production.append({
            'date': date,
            'production': production,
            'quota': quota,
            'compliance': (quota - production) / quota * 100
        })

    def add_demand_estimate(self, date, demand, yoy_change):
        self.demand_estimates.append({
            'date': date,
            'demand': demand,
            'yoy_change': yoy_change
        })

    def inventory_trend(self, weeks=4):
        if len(self.inventories) < weeks + 1:
            return 'insufficient_data'

        changes = [self.inventories[-i]['total'] - self.inventories[-i-1]['total'] 
                   for i in range(1, weeks + 1)]
        draws = sum(1 for c in changes if c < 0)

        return 'drawing' if draws >= weeks - 1 else ('building' if draws <= 1 else 'mixed')

    def supply_demand_balance(self):
        if not self.opec_production or not self.demand_estimates:
            return {'balance': 'no_data'}

        supply = self.opec_production[-1]['production']
        demand = self.demand_estimates[-1]['demand']
        balance = supply - demand

        return {
            'supply': supply,
            'demand': demand,
            'balance': balance,
            'status': 'surplus' if balance > 0.5 else ('deficit' if balance < -0.5 else 'balanced')
        }

    def generate_signal(self):
        inv_trend = self.inventory_trend()
        sd_balance = self.supply_demand_balance()

        score = 0
        if inv_trend == 'drawing': score += 1
        elif inv_trend == 'building': score -= 1

        if sd_balance.get('status') == 'deficit': score += 1
        elif sd_balance.get('status') == 'surplus': score -= 1

        signal = 'buy' if score > 0 else ('sell' if score < 0 else 'neutral')
        return {'signal': signal, 'score': score, 'inventory_trend': inv_trend, 'sd_status': sd_balance.get('status')}

Exercise 6: Commodity Currency Strategy (Open-ended)

Build a trading strategy that exploits commodity-currency relationships: - Track multiple commodity-currency pairs - Detect correlation breakdowns - Generate pair trading signals - Manage portfolio of commodity FX trades

# Exercise 6: Commodity Currency Strategy (Open-ended)
#
# Requirements:
# 1. Create class CommodityCurrencyStrategy
# 2. Define relationships: AUD-Gold, CAD-Oil, NOK-Brent
# 3. Track correlations over rolling windows
# 4. Detect divergences (correlation breakdown)
# 5. Generate signals: follow commodity or trade divergence
# 6. Rank opportunities by strength
#
# Your implementation:
Solution 6
class CommodityCurrencyStrategy:
    PAIRS = {
        'AUDUSD': {'commodity': 'gold', 'expected_corr': 0.6},
        'USDCAD': {'commodity': 'oil', 'expected_corr': -0.5},  # Inverted
        'USDNOK': {'commodity': 'brent', 'expected_corr': -0.6}
    }

    def __init__(self):
        self.currencies: Dict[str, pd.Series] = {}
        self.commodities: Dict[str, pd.Series] = {}

    def add_data(self, symbol: str, prices: pd.Series, is_commodity: bool = False):
        if is_commodity:
            self.commodities[symbol] = prices
        else:
            self.currencies[symbol] = prices

    def rolling_correlation(self, pair: str, window: int = 20) -> float:
        if pair not in self.PAIRS:
            return 0.0

        commodity = self.PAIRS[pair]['commodity']
        if pair not in self.currencies or commodity not in self.commodities:
            return 0.0

        curr_ret = self.currencies[pair].pct_change()
        comm_ret = self.commodities[commodity].pct_change()

        aligned = pd.concat([curr_ret, comm_ret], axis=1).dropna()
        return aligned.iloc[-window:, 0].corr(aligned.iloc[-window:, 1])

    def detect_divergence(self, pair: str, threshold: float = 0.3) -> Dict:
        expected = self.PAIRS[pair]['expected_corr']
        actual = self.rolling_correlation(pair)

        divergence = abs(actual - expected) > threshold
        return {
            'pair': pair,
            'expected': expected,
            'actual': actual,
            'divergence': divergence
        }

    def generate_signals(self) -> List[Dict]:
        signals = []

        for pair in self.PAIRS:
            div = self.detect_divergence(pair)
            commodity = self.PAIRS[pair]['commodity']

            if commodity not in self.commodities:
                continue

            comm_ret = (self.commodities[commodity].iloc[-1] / 
                       self.commodities[commodity].iloc[-20] - 1) * 100

            if div['divergence']:
                signal = 'divergence_trade'
            elif comm_ret > 3:
                signal = 'buy' if self.PAIRS[pair]['expected_corr'] > 0 else 'sell'
            elif comm_ret < -3:
                signal = 'sell' if self.PAIRS[pair]['expected_corr'] > 0 else 'buy'
            else:
                signal = 'neutral'

            signals.append({
                'pair': pair,
                'signal': signal,
                'commodity_return': comm_ret,
                'correlation': div['actual'],
                'strength': abs(comm_ret) / 10
            })

        return sorted(signals, key=lambda x: x['strength'], reverse=True)

Module Project: Commodity Trading System

Build a production-ready system that integrates all commodity trading concepts.

class CommodityTradingSystem:
    """
    Production-ready commodity trading system.
    
    Integrates: Gold, Oil, Agricultural, Commodity Currencies.
    Provides: Market analysis, Trading signals, Risk assessment.
    """
    
    def __init__(self):
        self.gold_analyzer = GoldAnalyzer()
        self.oil_analyzer = OilAnalyzer()
        self.commodity_currencies = CommodityCurrencyAnalyzer()
        self.market_data: Dict[str, pd.Series] = {}
    
    def load_market_data(self, symbol: str, prices: pd.Series) -> None:
        """Load price data for any market."""
        self.market_data[symbol] = prices
        
        # Route to appropriate analyzer
        if symbol == 'GOLD' or symbol == 'XAUUSD':
            self.gold_analyzer.set_data(gold=prices)
        elif symbol in ['OIL', 'WTI', 'CL']:
            self.oil_analyzer.set_prices(prices)
        elif symbol in ['AUD', 'CAD', 'NOK', 'NZD']:
            self.commodity_currencies.add_currency(symbol, prices)
        elif symbol.lower() in ['gold', 'oil', 'brent', 'iron_ore', 'dairy']:
            self.commodity_currencies.add_commodity(symbol.lower(), prices)
    
    def add_oil_inventory(self, date: datetime, actual: float,
                         forecast: float, previous: float) -> None:
        """Add oil inventory report."""
        self.oil_analyzer.add_inventory_report(date, actual, forecast, previous)
    
    def get_gold_analysis(self) -> Dict:
        """Get comprehensive gold analysis."""
        return {
            'safe_haven': self.gold_analyzer.safe_haven_signal(),
            'real_yield': self.gold_analyzer.real_yield_signal(),
            'combined': self.gold_analyzer.combined_signal()
        }
    
    def get_oil_analysis(self) -> Dict:
        """Get comprehensive oil analysis."""
        return {
            'inventory_trend': self.oil_analyzer.inventory_trend(),
            'inventory_signal': self.oil_analyzer.inventory_signal()
        }
    
    def get_commodity_fx_signals(self) -> List[Dict]:
        """Get all commodity currency signals."""
        signals = []
        for currency in ['AUD', 'CAD', 'NOK', 'NZD']:
            if currency in self.commodity_currencies.currency_data:
                signal = self.commodity_currencies.commodity_currency_signal(currency)
                divergence = self.commodity_currencies.check_divergence(currency)
                signals.append({
                    'currency': currency,
                    'signal': signal,
                    'divergence': divergence
                })
        return signals
    
    def get_all_opportunities(self) -> List[Dict]:
        """Rank all trading opportunities."""
        opportunities = []
        
        # Gold opportunity
        gold_signal = self.gold_analyzer.combined_signal()
        if gold_signal['overall'] in ['buy', 'strong_buy', 'sell', 'strong_sell']:
            opportunities.append({
                'market': 'GOLD',
                'direction': 'long' if 'buy' in gold_signal['overall'] else 'short',
                'strength': abs(gold_signal['score']) / 4,
                'reason': 'Combined fundamental signals'
            })
        
        # Oil opportunity
        oil_signal = self.oil_analyzer.inventory_signal()
        if oil_signal.get('signal') in ['buy', 'strong_buy', 'sell', 'strong_sell']:
            opportunities.append({
                'market': 'OIL',
                'direction': 'long' if 'buy' in oil_signal['signal'] else 'short',
                'strength': 0.7 if 'strong' in oil_signal['signal'] else 0.5,
                'reason': oil_signal.get('reason', '')
            })
        
        # Commodity currencies
        for fx_signal in self.get_commodity_fx_signals():
            sig = fx_signal['signal'].get('signal', 'neutral')
            if sig in ['buy_currency', 'sell_currency']:
                opportunities.append({
                    'market': f"{fx_signal['currency']}USD",
                    'direction': 'long' if sig == 'buy_currency' else 'short',
                    'strength': 0.5,
                    'reason': 'Commodity correlation'
                })
        
        return sorted(opportunities, key=lambda x: x['strength'], reverse=True)
    
    def print_dashboard(self) -> None:
        """Print comprehensive dashboard."""
        print("\n" + "=" * 60)
        print("  COMMODITY TRADING DASHBOARD")
        print(f"  {datetime.now().strftime('%Y-%m-%d %H:%M')}")
        print("=" * 60)
        
        # Gold Analysis
        print("\n" + "-" * 40)
        print("GOLD ANALYSIS")
        print("-" * 40)
        gold = self.get_gold_analysis()
        print(f"Safe Haven: {gold['safe_haven'].get('signal', 'N/A')}")
        print(f"Real Yield: {gold['real_yield'].get('signal', 'N/A')}")
        print(f"Combined: {gold['combined'].get('overall', 'N/A')}")
        
        # Oil Analysis
        print("\n" + "-" * 40)
        print("OIL ANALYSIS")
        print("-" * 40)
        oil = self.get_oil_analysis()
        print(f"Inventory Trend: {oil['inventory_trend'].get('trend', 'N/A')}")
        print(f"Signal: {oil['inventory_signal'].get('signal', 'N/A')}")
        
        # Commodity Currencies
        print("\n" + "-" * 40)
        print("COMMODITY CURRENCIES")
        print("-" * 40)
        for fx in self.get_commodity_fx_signals():
            print(f"{fx['currency']}: {fx['signal'].get('signal', 'N/A')}")
        
        # Top Opportunities
        print("\n" + "-" * 40)
        print("TOP OPPORTUNITIES")
        print("-" * 40)
        for i, opp in enumerate(self.get_all_opportunities()[:5], 1):
            print(f"{i}. {opp['market']} - {opp['direction'].upper()}")
            print(f"   Strength: {opp['strength']:.0%} | {opp['reason']}")
        
        print("\n" + "=" * 60)
# Demo the system
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')

system = CommodityTradingSystem()

# Load market data
system.load_market_data('GOLD', pd.Series(2000 + np.cumsum(np.random.normal(1, 15, 60)), index=dates))
system.load_market_data('OIL', pd.Series(75 + np.cumsum(np.random.normal(0.2, 1, 60)), index=dates))
system.load_market_data('AUD', pd.Series(0.65 + np.cumsum(np.random.normal(0.0005, 0.003, 60)), index=dates))
system.load_market_data('CAD', pd.Series(1.35 + np.cumsum(np.random.normal(-0.0003, 0.002, 60)), index=dates))
system.load_market_data('gold', pd.Series(2000 + np.cumsum(np.random.normal(1, 15, 60)), index=dates))
system.load_market_data('oil', pd.Series(75 + np.cumsum(np.random.normal(0.2, 1, 60)), index=dates))

# Set additional gold data
system.gold_analyzer.set_data(
    gold=system.market_data['GOLD'],
    dxy=pd.Series(104 + np.cumsum(np.random.normal(-0.05, 0.3, 60)), index=dates),
    vix=pd.Series(np.clip(18 + np.cumsum(np.random.normal(0.1, 1, 60)), 10, 40), index=dates),
    real_yields=pd.Series(0.5 + np.cumsum(np.random.normal(-0.01, 0.05, 60)), index=dates)
)

# Add oil inventory data
system.add_oil_inventory(datetime(2024, 4, 3), 451.2, 453.0, 452.5)
system.add_oil_inventory(datetime(2024, 4, 10), 449.8, 451.0, 451.2)
system.add_oil_inventory(datetime(2024, 4, 17), 448.5, 450.5, 449.8)
system.add_oil_inventory(datetime(2024, 4, 24), 446.2, 449.0, 448.5)

# Print dashboard
system.print_dashboard()

Key Takeaways

  • Gold Trading: Driven by USD, real yields, and safe-haven demand; negative correlation with risk assets
  • Oil Analysis: Track EIA inventories (Wed), OPEC decisions, and supply/demand balance
  • Inventory Impact: Draws = bullish, Builds = bearish; surprises move markets most
  • Agricultural Seasonality: Strong patterns around planting/harvest; weather is key risk
  • Commodity Currencies: AUD-Gold, CAD-Oil correlations create trading opportunities
  • Divergence Trading: When correlations break down, mean reversion opportunities emerge
  • Multi-Asset Integration: Combine commodity and FX analysis for better signals
  • Risk Management: Commodities are volatile; size positions appropriately

Next: Module 8 - Trading Strategies where we'll build trend following, mean reversion, carry, and news trading systems.

Module 8: Trading Strategies

Part 2: Analysis & Strategies

Duration Exercises
~2.5 hours 6

Learning Objectives

  • Build forex trend following systems with MA and breakout strategies
  • Implement range trading and mean reversion strategies
  • Understand carry trade mechanics and interest rate differentials
  • Develop news trading strategies for high-impact events

Prerequisites

  • Modules 1-7 (Forex/Futures fundamentals, analysis, commodities)
  • Understanding of technical and fundamental analysis
  • Python pandas and backtesting concepts
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')

8.1 Forex Trend Following

Trend following strategies capture extended directional moves in currency pairs using moving averages and breakout systems.

Trend Following Approaches

Strategy Entry Signal Exit Signal Best Market
MA Crossover Fast MA crosses slow MA Opposite crossover Trending
Channel Breakout Price breaks N-period high/low Opposite breakout Volatile
ADX Filter Trade with trend when ADX > 25 ADX falls below 20 Strong trends
Donchian Price breaks 20-day high/low 10-day opposite Any
class MovingAverageCrossover:
    """Classic moving average crossover trend following strategy."""
    
    def __init__(self, fast_period: int = 20, slow_period: int = 50):
        self.fast_period = fast_period
        self.slow_period = slow_period
        self.prices: pd.Series = pd.Series(dtype=float)
    
    def set_data(self, prices: pd.Series) -> None:
        """Set price data."""
        self.prices = prices
    
    def calculate_signals(self) -> pd.DataFrame:
        """Calculate MA crossover signals."""
        df = pd.DataFrame({'price': self.prices})
        
        df['fast_ma'] = df['price'].rolling(self.fast_period).mean()
        df['slow_ma'] = df['price'].rolling(self.slow_period).mean()
        
        # Signal: 1 = long, -1 = short, 0 = no position
        df['signal'] = 0
        df.loc[df['fast_ma'] > df['slow_ma'], 'signal'] = 1
        df.loc[df['fast_ma'] < df['slow_ma'], 'signal'] = -1
        
        # Trade signals (changes in position)
        df['trade'] = df['signal'].diff()
        
        return df
    
    def get_current_signal(self) -> Dict:
        """Get current trading signal."""
        df = self.calculate_signals()
        
        if len(df) < self.slow_period:
            return {'signal': 'no_data'}
        
        latest = df.iloc[-1]
        
        return {
            'signal': 'long' if latest['signal'] == 1 else ('short' if latest['signal'] == -1 else 'flat'),
            'fast_ma': latest['fast_ma'],
            'slow_ma': latest['slow_ma'],
            'price': latest['price'],
            'trade': 'buy' if latest['trade'] == 2 else ('sell' if latest['trade'] == -2 else 'hold')
        }
    
    def backtest(self) -> Dict:
        """Simple backtest of the strategy."""
        df = self.calculate_signals()
        df['returns'] = df['price'].pct_change()
        df['strategy_returns'] = df['signal'].shift(1) * df['returns']
        
        total_return = (1 + df['strategy_returns'].dropna()).prod() - 1
        buy_hold = (1 + df['returns'].dropna()).prod() - 1
        
        trades = df['trade'].abs().sum() / 2
        
        return {
            'strategy_return': total_return * 100,
            'buy_hold_return': buy_hold * 100,
            'outperformance': (total_return - buy_hold) * 100,
            'num_trades': int(trades),
            'sharpe': df['strategy_returns'].mean() / df['strategy_returns'].std() * np.sqrt(252) if df['strategy_returns'].std() > 0 else 0
        }

# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=120, freq='D')
# Create trending data
trend = np.cumsum(np.random.normal(0.001, 0.01, 120))
prices = pd.Series(1.1000 + trend, index=dates)

ma_strategy = MovingAverageCrossover(fast_period=10, slow_period=30)
ma_strategy.set_data(prices)

print("MA Crossover Strategy:")
print(f"Current Signal: {ma_strategy.get_current_signal()}")
print(f"\nBacktest Results: {ma_strategy.backtest()}")
class BreakoutStrategy:
    """Donchian channel breakout strategy."""
    
    def __init__(self, entry_period: int = 20, exit_period: int = 10):
        self.entry_period = entry_period
        self.exit_period = exit_period
        self.prices: pd.DataFrame = pd.DataFrame()
    
    def set_data(self, high: pd.Series, low: pd.Series, close: pd.Series) -> None:
        """Set OHLC data."""
        self.prices = pd.DataFrame({
            'high': high,
            'low': low,
            'close': close
        })
    
    def calculate_channels(self) -> pd.DataFrame:
        """Calculate Donchian channels."""
        df = self.prices.copy()
        
        # Entry channels
        df['entry_high'] = df['high'].rolling(self.entry_period).max()
        df['entry_low'] = df['low'].rolling(self.entry_period).min()
        
        # Exit channels
        df['exit_high'] = df['high'].rolling(self.exit_period).max()
        df['exit_low'] = df['low'].rolling(self.exit_period).min()
        
        return df
    
    def calculate_signals(self) -> pd.DataFrame:
        """Calculate breakout signals."""
        df = self.calculate_channels()
        
        df['signal'] = 0
        position = 0
        signals = []
        
        for i in range(len(df)):
            if i < self.entry_period:
                signals.append(0)
                continue
            
            close = df['close'].iloc[i]
            prev_entry_high = df['entry_high'].iloc[i-1]
            prev_entry_low = df['entry_low'].iloc[i-1]
            prev_exit_high = df['exit_high'].iloc[i-1]
            prev_exit_low = df['exit_low'].iloc[i-1]
            
            # Entry signals
            if position == 0:
                if close > prev_entry_high:
                    position = 1
                elif close < prev_entry_low:
                    position = -1
            # Exit signals
            elif position == 1:
                if close < prev_exit_low:
                    position = 0
            elif position == -1:
                if close > prev_exit_high:
                    position = 0
            
            signals.append(position)
        
        df['signal'] = signals
        df['trade'] = df['signal'].diff()
        
        return df
    
    def get_current_signal(self) -> Dict:
        """Get current signal and levels."""
        df = self.calculate_signals()
        latest = df.iloc[-1]
        
        return {
            'position': 'long' if latest['signal'] == 1 else ('short' if latest['signal'] == -1 else 'flat'),
            'entry_high': latest['entry_high'],
            'entry_low': latest['entry_low'],
            'close': latest['close'],
            'trade': 'entry' if abs(latest['trade']) > 0 else 'hold'
        }

# Demo
np.random.seed(42)
high = pd.Series(1.1000 + trend + np.random.uniform(0, 0.005, 120), index=dates)
low = pd.Series(1.1000 + trend - np.random.uniform(0, 0.005, 120), index=dates)
close = prices

breakout = BreakoutStrategy(entry_period=20, exit_period=10)
breakout.set_data(high, low, close)

print("Breakout Strategy:")
print(f"Current Signal: {breakout.get_current_signal()}")
class TrendFilter:
    """ADX-based trend filter to improve trend following signals."""
    
    def __init__(self, adx_period: int = 14, adx_threshold: float = 25):
        self.adx_period = adx_period
        self.adx_threshold = adx_threshold
    
    def calculate_adx(self, high: pd.Series, low: pd.Series, 
                      close: pd.Series) -> pd.DataFrame:
        """Calculate ADX indicator."""
        df = pd.DataFrame({'high': high, 'low': low, 'close': close})
        
        # True Range
        df['tr1'] = df['high'] - df['low']
        df['tr2'] = abs(df['high'] - df['close'].shift(1))
        df['tr3'] = abs(df['low'] - df['close'].shift(1))
        df['tr'] = df[['tr1', 'tr2', 'tr3']].max(axis=1)
        
        # Directional Movement
        df['up_move'] = df['high'] - df['high'].shift(1)
        df['down_move'] = df['low'].shift(1) - df['low']
        
        df['+dm'] = np.where((df['up_move'] > df['down_move']) & (df['up_move'] > 0), df['up_move'], 0)
        df['-dm'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0)
        
        # Smoothed values
        df['atr'] = df['tr'].rolling(self.adx_period).mean()
        df['+di'] = 100 * (df['+dm'].rolling(self.adx_period).mean() / df['atr'])
        df['-di'] = 100 * (df['-dm'].rolling(self.adx_period).mean() / df['atr'])
        
        # ADX
        df['dx'] = 100 * abs(df['+di'] - df['-di']) / (df['+di'] + df['-di'])
        df['adx'] = df['dx'].rolling(self.adx_period).mean()
        
        return df[['adx', '+di', '-di']]
    
    def is_trending(self, high: pd.Series, low: pd.Series, close: pd.Series) -> Dict:
        """Check if market is trending."""
        adx_df = self.calculate_adx(high, low, close)
        
        if adx_df['adx'].isna().iloc[-1]:
            return {'trending': False, 'reason': 'Insufficient data'}
        
        current_adx = adx_df['adx'].iloc[-1]
        plus_di = adx_df['+di'].iloc[-1]
        minus_di = adx_df['-di'].iloc[-1]
        
        trending = current_adx > self.adx_threshold
        direction = 'up' if plus_di > minus_di else 'down'
        
        return {
            'trending': trending,
            'adx': current_adx,
            'direction': direction if trending else 'none',
            'strength': 'strong' if current_adx > 40 else ('moderate' if current_adx > 25 else 'weak')
        }

# Demo
trend_filter = TrendFilter(adx_period=14, adx_threshold=25)
result = trend_filter.is_trending(high, low, close)
print(f"Trend Filter: {result}")

8.2 Range & Mean Reversion

Mean reversion strategies profit from price returning to equilibrium levels after temporary deviations.

class RangeDetector:
    """Detect ranging vs trending markets."""
    
    def __init__(self, lookback: int = 20):
        self.lookback = lookback
    
    def detect_range(self, high: pd.Series, low: pd.Series, 
                     close: pd.Series) -> Dict:
        """Detect if market is in a range."""
        # Calculate range metrics
        period_high = high.rolling(self.lookback).max().iloc[-1]
        period_low = low.rolling(self.lookback).min().iloc[-1]
        range_size = period_high - period_low
        
        # Calculate ATR for comparison
        tr = pd.concat([
            high - low,
            abs(high - close.shift(1)),
            abs(low - close.shift(1))
        ], axis=1).max(axis=1)
        atr = tr.rolling(14).mean().iloc[-1]
        
        # Range ratio: low ratio = ranging, high ratio = trending
        range_ratio = range_size / (atr * self.lookback)
        
        # Current position within range
        current = close.iloc[-1]
        position_in_range = (current - period_low) / range_size if range_size > 0 else 0.5
        
        is_ranging = range_ratio < 0.5
        
        return {
            'is_ranging': is_ranging,
            'range_high': period_high,
            'range_low': period_low,
            'range_size': range_size,
            'range_ratio': range_ratio,
            'position_in_range': position_in_range,
            'near_resistance': position_in_range > 0.8,
            'near_support': position_in_range < 0.2
        }

# Demo with ranging data
np.random.seed(123)
range_prices = 1.1000 + np.sin(np.linspace(0, 4*np.pi, 100)) * 0.01 + np.random.normal(0, 0.002, 100)
range_dates = pd.date_range('2024-01-01', periods=100, freq='D')

range_high = pd.Series(range_prices + np.random.uniform(0, 0.002, 100), index=range_dates)
range_low = pd.Series(range_prices - np.random.uniform(0, 0.002, 100), index=range_dates)
range_close = pd.Series(range_prices, index=range_dates)

detector = RangeDetector(lookback=20)
print(f"Range Detection: {detector.detect_range(range_high, range_low, range_close)}")
class MeanReversionStrategy:
    """Mean reversion strategy using Bollinger Bands and RSI."""
    
    def __init__(self, bb_period: int = 20, bb_std: float = 2.0,
                 rsi_period: int = 14, oversold: float = 30, overbought: float = 70):
        self.bb_period = bb_period
        self.bb_std = bb_std
        self.rsi_period = rsi_period
        self.oversold = oversold
        self.overbought = overbought
        self.prices: pd.Series = pd.Series(dtype=float)
    
    def set_data(self, prices: pd.Series) -> None:
        """Set price data."""
        self.prices = prices
    
    def calculate_indicators(self) -> pd.DataFrame:
        """Calculate Bollinger Bands and RSI."""
        df = pd.DataFrame({'price': self.prices})
        
        # Bollinger Bands
        df['bb_middle'] = df['price'].rolling(self.bb_period).mean()
        df['bb_std'] = df['price'].rolling(self.bb_period).std()
        df['bb_upper'] = df['bb_middle'] + self.bb_std * df['bb_std']
        df['bb_lower'] = df['bb_middle'] - self.bb_std * df['bb_std']
        
        # RSI
        delta = df['price'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
        rs = gain / loss
        df['rsi'] = 100 - (100 / (1 + rs))
        
        # %B indicator (position within bands)
        df['percent_b'] = (df['price'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
        
        return df
    
    def calculate_signals(self) -> pd.DataFrame:
        """Calculate mean reversion signals."""
        df = self.calculate_indicators()
        
        df['signal'] = 0
        
        # Buy when oversold (price below lower band AND RSI oversold)
        df.loc[(df['price'] < df['bb_lower']) & (df['rsi'] < self.oversold), 'signal'] = 1
        
        # Sell when overbought (price above upper band AND RSI overbought)
        df.loc[(df['price'] > df['bb_upper']) & (df['rsi'] > self.overbought), 'signal'] = -1
        
        return df
    
    def get_current_signal(self) -> Dict:
        """Get current signal."""
        df = self.calculate_indicators()
        latest = df.iloc[-1]
        
        # Determine condition
        if latest['price'] < latest['bb_lower'] and latest['rsi'] < self.oversold:
            signal = 'buy'
            condition = 'oversold'
        elif latest['price'] > latest['bb_upper'] and latest['rsi'] > self.overbought:
            signal = 'sell'
            condition = 'overbought'
        elif latest['percent_b'] < 0.2:
            signal = 'watch_buy'
            condition = 'approaching_oversold'
        elif latest['percent_b'] > 0.8:
            signal = 'watch_sell'
            condition = 'approaching_overbought'
        else:
            signal = 'neutral'
            condition = 'normal'
        
        return {
            'signal': signal,
            'condition': condition,
            'price': latest['price'],
            'bb_upper': latest['bb_upper'],
            'bb_lower': latest['bb_lower'],
            'rsi': latest['rsi'],
            'percent_b': latest['percent_b']
        }

# Demo
mr_strategy = MeanReversionStrategy()
mr_strategy.set_data(range_close)

print("Mean Reversion Strategy:")
print(f"Current Signal: {mr_strategy.get_current_signal()}")

8.3 Carry Trade

Carry trade profits from interest rate differentials by going long high-yield currencies and short low-yield currencies.

Carry Trade Mechanics

Component Description
Interest Differential Profit from holding high-yield vs low-yield
Swap Points Daily credit/debit based on rate differential
Risk Currency depreciation can wipe out carry gains
Best Conditions Low volatility, risk-on environment
class CarryTradeCalculator:
    """Calculate carry trade opportunities and returns."""
    
    def __init__(self):
        self.rates: Dict[str, float] = {}
        self.prices: Dict[str, pd.Series] = {}
    
    def set_rate(self, currency: str, rate: float) -> None:
        """Set interest rate for currency."""
        self.rates[currency] = rate
    
    def set_price(self, pair: str, prices: pd.Series) -> None:
        """Set price data for currency pair."""
        self.prices[pair] = prices
    
    def calculate_carry(self, long_currency: str, short_currency: str) -> Dict:
        """Calculate annual carry for a trade."""
        if long_currency not in self.rates or short_currency not in self.rates:
            return {'carry': 0, 'error': 'Missing rate data'}
        
        long_rate = self.rates[long_currency]
        short_rate = self.rates[short_currency]
        carry = long_rate - short_rate
        daily_carry = carry / 365
        
        return {
            'long_currency': long_currency,
            'short_currency': short_currency,
            'long_rate': long_rate,
            'short_rate': short_rate,
            'annual_carry': carry,
            'daily_carry': daily_carry,
            'monthly_carry': carry / 12
        }
    
    def get_best_carry_pairs(self) -> List[Dict]:
        """Find best carry trade opportunities."""
        pairs = []
        currencies = list(self.rates.keys())
        
        for i, c1 in enumerate(currencies):
            for c2 in currencies[i+1:]:
                if self.rates[c1] > self.rates[c2]:
                    carry = self.calculate_carry(c1, c2)
                else:
                    carry = self.calculate_carry(c2, c1)
                
                pairs.append(carry)
        
        return sorted(pairs, key=lambda x: x['annual_carry'], reverse=True)
    
    def calculate_total_return(self, pair: str, long_currency: str, 
                               short_currency: str, days: int = 30) -> Dict:
        """Calculate total return (carry + price change)."""
        carry = self.calculate_carry(long_currency, short_currency)
        
        if pair not in self.prices or len(self.prices[pair]) < days:
            return {'error': 'Insufficient price data'}
        
        prices = self.prices[pair]
        price_return = (prices.iloc[-1] / prices.iloc[-days] - 1) * 100
        carry_return = carry['annual_carry'] * days / 365
        
        # Adjust direction based on pair convention
        if pair.startswith(long_currency):
            total_return = price_return + carry_return
        else:
            total_return = -price_return + carry_return
        
        return {
            'pair': pair,
            'days': days,
            'price_return': price_return,
            'carry_return': carry_return,
            'total_return': total_return,
            'carry_helped': carry_return > 0
        }

# Demo
carry_calc = CarryTradeCalculator()

# Set interest rates
carry_calc.set_rate('USD', 5.25)
carry_calc.set_rate('EUR', 4.50)
carry_calc.set_rate('JPY', 0.10)
carry_calc.set_rate('AUD', 4.35)
carry_calc.set_rate('CHF', 1.75)

print("Best Carry Pairs:")
for pair in carry_calc.get_best_carry_pairs()[:3]:
    print(f"  Long {pair['long_currency']}/Short {pair['short_currency']}: {pair['annual_carry']:.2f}%")
class CarryTradeStrategy:
    """Full carry trade strategy with risk management."""
    
    def __init__(self, min_carry: float = 2.0, max_volatility: float = 15.0):
        self.min_carry = min_carry  # Minimum carry to consider
        self.max_volatility = max_volatility  # Maximum acceptable volatility
        self.calculator = CarryTradeCalculator()
        self.volatility: Dict[str, float] = {}
        self.vix: float = 0
    
    def set_rate(self, currency: str, rate: float) -> None:
        """Set interest rate."""
        self.calculator.set_rate(currency, rate)
    
    def set_volatility(self, pair: str, vol: float) -> None:
        """Set annualized volatility for pair."""
        self.volatility[pair] = vol
    
    def set_vix(self, vix: float) -> None:
        """Set current VIX level."""
        self.vix = vix
    
    def risk_on_environment(self) -> bool:
        """Check if risk environment supports carry."""
        return self.vix < 20
    
    def calculate_risk_adjusted_carry(self, long_curr: str, short_curr: str,
                                      pair: str) -> Dict:
        """Calculate carry adjusted for volatility risk."""
        carry = self.calculator.calculate_carry(long_curr, short_curr)
        vol = self.volatility.get(pair, 10.0)
        
        # Sharpe-like ratio: carry / volatility
        carry_ratio = carry['annual_carry'] / vol if vol > 0 else 0
        
        return {
            **carry,
            'pair': pair,
            'volatility': vol,
            'carry_ratio': carry_ratio,
            'attractive': carry['annual_carry'] >= self.min_carry and vol <= self.max_volatility
        }
    
    def generate_signals(self) -> List[Dict]:
        """Generate carry trade signals."""
        if not self.risk_on_environment():
            return [{'signal': 'risk_off', 'reason': f'VIX at {self.vix}, avoid carry'}]
        
        signals = []
        best_pairs = self.calculator.get_best_carry_pairs()
        
        for pair_info in best_pairs:
            # Construct pair name
            pair = f"{pair_info['long_currency']}{pair_info['short_currency']}"
            
            if pair_info['annual_carry'] >= self.min_carry:
                vol = self.volatility.get(pair, 10.0)
                
                if vol <= self.max_volatility:
                    signals.append({
                        'signal': 'carry_long',
                        'pair': pair,
                        'carry': pair_info['annual_carry'],
                        'volatility': vol,
                        'carry_ratio': pair_info['annual_carry'] / vol,
                        'confidence': 'high' if pair_info['annual_carry'] / vol > 0.5 else 'medium'
                    })
        
        return sorted(signals, key=lambda x: x.get('carry_ratio', 0), reverse=True)

# Demo
carry_strategy = CarryTradeStrategy(min_carry=3.0, max_volatility=12.0)

# Set rates
carry_strategy.set_rate('USD', 5.25)
carry_strategy.set_rate('EUR', 4.50)
carry_strategy.set_rate('JPY', 0.10)
carry_strategy.set_rate('AUD', 4.35)

# Set volatilities
carry_strategy.set_volatility('USDJPY', 8.5)
carry_strategy.set_volatility('AUDJPY', 11.0)
carry_strategy.set_volatility('EURJPY', 9.0)

# Set VIX
carry_strategy.set_vix(16.5)

print("Carry Trade Signals:")
for signal in carry_strategy.generate_signals():
    print(f"  {signal}")

8.4 News Trading

News trading strategies capitalize on high-impact economic releases and central bank decisions.

class NewsEvent:
    """Represents a high-impact news event."""
    
    def __init__(self, name: str, currency: str, release_time: datetime,
                 forecast: float, previous: float, impact: str = 'high'):
        self.name = name
        self.currency = currency
        self.release_time = release_time
        self.forecast = forecast
        self.previous = previous
        self.impact = impact
        self.actual: Optional[float] = None
    
    def set_actual(self, actual: float) -> None:
        """Set actual release value."""
        self.actual = actual
    
    @property
    def surprise(self) -> Optional[float]:
        """Calculate surprise (actual - forecast)."""
        if self.actual is not None:
            return self.actual - self.forecast
        return None
    
    @property
    def surprise_pct(self) -> Optional[float]:
        """Calculate surprise as percentage."""
        if self.actual is not None and self.forecast != 0:
            return (self.actual - self.forecast) / abs(self.forecast) * 100
        return None


class NewsTrader:
    """News trading strategy implementation."""
    
    # Expected impact of surprise on currency (pips per 1% surprise)
    EVENT_SENSITIVITY = {
        'NFP': 30,
        'CPI': 25,
        'GDP': 20,
        'Rate Decision': 50,
        'Retail Sales': 15,
        'PMI': 10
    }
    
    def __init__(self):
        self.events: List[NewsEvent] = []
        self.historical_reactions: List[Dict] = []
    
    def add_event(self, event: NewsEvent) -> None:
        """Add upcoming or past event."""
        self.events.append(event)
    
    def record_reaction(self, event_name: str, currency: str,
                       surprise_pct: float, price_move_pips: float) -> None:
        """Record historical price reaction to event."""
        self.historical_reactions.append({
            'event': event_name,
            'currency': currency,
            'surprise_pct': surprise_pct,
            'price_move': price_move_pips,
            'sensitivity': price_move_pips / surprise_pct if surprise_pct != 0 else 0
        })
    
    def estimate_move(self, event: NewsEvent, actual: float) -> Dict:
        """Estimate expected price move based on surprise."""
        event.set_actual(actual)
        surprise_pct = event.surprise_pct
        
        if surprise_pct is None:
            return {'error': 'No surprise calculated'}
        
        # Get sensitivity for this event type
        sensitivity = self.EVENT_SENSITIVITY.get(event.name, 15)
        
        # Check historical reactions for this specific event
        hist = [r for r in self.historical_reactions if r['event'] == event.name]
        if hist:
            sensitivity = np.mean([r['sensitivity'] for r in hist])
        
        expected_move = surprise_pct * sensitivity
        
        return {
            'event': event.name,
            'currency': event.currency,
            'actual': actual,
            'forecast': event.forecast,
            'surprise_pct': surprise_pct,
            'expected_move_pips': expected_move,
            'direction': 'bullish' if expected_move > 0 else 'bearish',
            'magnitude': 'large' if abs(expected_move) > 50 else ('medium' if abs(expected_move) > 20 else 'small')
        }
    
    def straddle_setup(self, event: NewsEvent, entry_distance_pips: float = 20) -> Dict:
        """Set up a straddle trade before news release."""
        sensitivity = self.EVENT_SENSITIVITY.get(event.name, 15)
        
        # Estimate potential move based on typical surprise
        typical_surprise = 2.0  # Assume 2% typical surprise
        expected_move = typical_surprise * sensitivity
        
        return {
            'strategy': 'straddle',
            'event': event.name,
            'release_time': event.release_time,
            'buy_stop_distance': entry_distance_pips,
            'sell_stop_distance': entry_distance_pips,
            'target_pips': expected_move,
            'stop_loss_pips': entry_distance_pips * 1.5,
            'risk_reward': expected_move / (entry_distance_pips * 1.5),
            'recommendation': 'trade' if expected_move / (entry_distance_pips * 1.5) > 1.5 else 'skip'
        }

# Demo
news_trader = NewsTrader()

# Create NFP event
nfp = NewsEvent(
    name='NFP',
    currency='USD',
    release_time=datetime(2024, 5, 3, 8, 30),
    forecast=240,
    previous=303
)
news_trader.add_event(nfp)

# Record some historical reactions
news_trader.record_reaction('NFP', 'USD', 5.0, 45)
news_trader.record_reaction('NFP', 'USD', -3.0, -30)
news_trader.record_reaction('NFP', 'USD', 10.0, 85)

print("Straddle Setup:")
print(news_trader.straddle_setup(nfp))

print("\nAfter Release (Actual = 275):")
print(news_trader.estimate_move(nfp, 275))
class FadeStrategy:
    """Fade (counter-trend) strategy after news spikes."""
    
    def __init__(self, fade_threshold_pips: float = 50, 
                 wait_minutes: int = 15):
        self.fade_threshold = fade_threshold_pips
        self.wait_minutes = wait_minutes
    
    def analyze_spike(self, pre_news_price: float, spike_price: float,
                      current_price: float, pip_value: float = 0.0001) -> Dict:
        """Analyze post-news spike for fade opportunity."""
        spike_pips = (spike_price - pre_news_price) / pip_value
        current_pips = (current_price - pre_news_price) / pip_value
        
        # Calculate retracement
        if spike_pips != 0:
            retracement = 1 - (current_pips / spike_pips)
        else:
            retracement = 0
        
        return {
            'spike_pips': spike_pips,
            'current_from_pre': current_pips,
            'retracement': retracement * 100,
            'spike_direction': 'up' if spike_pips > 0 else 'down',
            'extended': abs(spike_pips) > self.fade_threshold
        }
    
    def generate_fade_signal(self, pre_news: float, spike: float,
                            current: float, pip_value: float = 0.0001) -> Dict:
        """Generate fade trade signal."""
        analysis = self.analyze_spike(pre_news, spike, current, pip_value)
        
        if not analysis['extended']:
            return {'signal': 'no_trade', 'reason': 'Spike not extended enough'}
        
        # Fade if retracement is small (spike still extended)
        if analysis['retracement'] < 25:
            signal = 'sell' if analysis['spike_direction'] == 'up' else 'buy'
            target = pre_news + (spike - pre_news) * 0.5  # Target 50% retracement
            stop = spike + (spike - pre_news) * 0.2  # Stop beyond spike
            
            return {
                'signal': signal,
                'entry': current,
                'target': target,
                'stop': stop,
                'risk_pips': abs(stop - current) / pip_value,
                'reward_pips': abs(target - current) / pip_value,
                'reason': f'Fade extended {analysis["spike_direction"]} spike'
            }
        
        return {'signal': 'no_trade', 'reason': 'Already retracing'}

# Demo
fade = FadeStrategy(fade_threshold_pips=40)

# Scenario: NFP spike up 60 pips, now only retraced 10 pips
pre_news = 1.1000
spike = 1.1060  # 60 pip spike up
current = 1.1050  # Only 10 pip retracement

print("Fade Analysis:")
print(fade.analyze_spike(pre_news, spike, current))
print("\nFade Signal:")
print(fade.generate_fade_signal(pre_news, spike, current))

Exercises

Exercise 1: Triple MA Strategy (Guided)

Complete the TripleMAStrategy class that uses three moving averages for trend confirmation.

class TripleMAStrategy:
    """Triple moving average trend following strategy."""
    
    def __init__(self, fast: int = 10, medium: int = 20, slow: int = 50):
        self.fast = fast
        self.medium = medium
        self.slow = slow
        self.prices: pd.Series = pd.Series(dtype=float)
    
    def set_data(self, prices: pd.Series) -> None:
        """Set price data."""
        self.prices = prices
    
    def calculate_mas(self) -> pd.DataFrame:
        """Calculate all three MAs."""
        df = pd.DataFrame({'price': self.prices})
        df['fast_ma'] = df['price'].rolling(self.fast).______
        df['medium_ma'] = df['price'].rolling(self.medium).mean()
        df['slow_ma'] = df['price'].rolling(self.slow).mean()
        return df
    
    def get_alignment(self) -> str:
        """Check MA alignment."""
        df = self.calculate_mas()
        latest = df.iloc[-1]
        
        if latest['fast_ma'] > latest['medium_ma'] > latest['______']:
            return 'bullish'
        elif latest['fast_ma'] < latest['medium_ma'] < latest['slow_ma']:
            return 'bearish'
        return 'mixed'
    
    def generate_signal(self) -> Dict:
        """Generate trading signal."""
        alignment = self.get_alignment()
        df = self.calculate_mas()
        
        # Check for pullback to medium MA in trending market
        latest = df.iloc[-1]
        near_medium = abs(latest['price'] - latest['medium_ma']) / latest['price'] < 0.005
        
        if alignment == 'bullish' and near_medium:
            return {'signal': '______', 'reason': 'Bullish alignment + pullback'}
        elif alignment == 'bearish' and near_medium:
            return {'signal': 'sell', 'reason': 'Bearish alignment + pullback'}
        elif alignment in ['bullish', 'bearish']:
            return {'signal': 'hold', 'reason': f'{alignment} but no pullback'}
        
        return {'signal': 'neutral', 'reason': 'No clear trend'}

# Test
triple_ma = TripleMAStrategy()
triple_ma.set_data(prices)
print(f"Alignment: {triple_ma.get_alignment()}")
print(f"Signal: {triple_ma.generate_signal()}")
Solution 1
class TripleMAStrategy:
    def __init__(self, fast: int = 10, medium: int = 20, slow: int = 50):
        self.fast = fast
        self.medium = medium
        self.slow = slow
        self.prices: pd.Series = pd.Series(dtype=float)

    def set_data(self, prices: pd.Series) -> None:
        self.prices = prices

    def calculate_mas(self) -> pd.DataFrame:
        df = pd.DataFrame({'price': self.prices})
        df['fast_ma'] = df['price'].rolling(self.fast).mean()
        df['medium_ma'] = df['price'].rolling(self.medium).mean()
        df['slow_ma'] = df['price'].rolling(self.slow).mean()
        return df

    def get_alignment(self) -> str:
        df = self.calculate_mas()
        latest = df.iloc[-1]

        if latest['fast_ma'] > latest['medium_ma'] > latest['slow_ma']:
            return 'bullish'
        elif latest['fast_ma'] < latest['medium_ma'] < latest['slow_ma']:
            return 'bearish'
        return 'mixed'

    def generate_signal(self) -> Dict:
        alignment = self.get_alignment()
        df = self.calculate_mas()
        latest = df.iloc[-1]
        near_medium = abs(latest['price'] - latest['medium_ma']) / latest['price'] < 0.005

        if alignment == 'bullish' and near_medium:
            return {'signal': 'buy', 'reason': 'Bullish alignment + pullback'}
        elif alignment == 'bearish' and near_medium:
            return {'signal': 'sell', 'reason': 'Bearish alignment + pullback'}
        elif alignment in ['bullish', 'bearish']:
            return {'signal': 'hold', 'reason': f'{alignment} but no pullback'}
        return {'signal': 'neutral', 'reason': 'No clear trend'}

Exercise 2: RSI Divergence Detector (Guided)

Complete the RSIDivergence class that detects bullish and bearish RSI divergences.

class RSIDivergence:
    """Detect RSI divergences for mean reversion signals."""
    
    def __init__(self, rsi_period: int = 14, lookback: int = 20):
        self.rsi_period = rsi_period
        self.lookback = lookback
        self.prices: pd.Series = pd.Series(dtype=float)
    
    def set_data(self, prices: pd.Series) -> None:
        """Set price data."""
        self.prices = prices
    
    def calculate_rsi(self) -> pd.Series:
        """Calculate RSI."""
        delta = self.prices.diff()
        gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).______
        loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
        rs = gain / loss
        return 100 - (100 / (1 + rs))
    
    def find_divergence(self) -> Dict:
        """Find bullish or bearish divergence."""
        rsi = self.calculate_rsi()
        prices = self.prices[-self.lookback:]
        rsi_recent = rsi[-self.lookback:]
        
        # Find local lows for bullish divergence
        price_low_idx = prices.______
        rsi_at_price_low = rsi_recent.iloc[prices.index.get_loc(price_low_idx) - (len(self.prices) - self.lookback)]
        
        current_price = prices.iloc[-1]
        current_rsi = rsi_recent.iloc[-1]
        
        # Bullish divergence: price makes lower low, RSI makes higher low
        if current_price < prices[price_low_idx] and current_rsi > rsi_at_price_low:
            return {
                'divergence': 'bullish',
                'signal': 'buy',
                'price_low': prices[price_low_idx],
                'current_price': current_price,
                'rsi_at_low': rsi_at_price_low,
                'current_rsi': current_rsi
            }
        
        # Check bearish (price higher high, RSI lower high)
        price_high_idx = prices.idxmax()
        rsi_at_price_high = rsi_recent.iloc[prices.index.get_loc(price_high_idx) - (len(self.prices) - self.lookback)]
        
        if current_price > prices[price_high_idx] and current_rsi < rsi_at_price_high:
            return {
                'divergence': '______',
                'signal': 'sell',
                'price_high': prices[price_high_idx],
                'current_price': current_price,
                'rsi_at_high': rsi_at_price_high,
                'current_rsi': current_rsi
            }
        
        return {'divergence': 'none', 'signal': 'neutral'}

# Test
div_detector = RSIDivergence()
div_detector.set_data(range_close)
print(f"Divergence: {div_detector.find_divergence()}")
Solution 2
class RSIDivergence:
    def __init__(self, rsi_period: int = 14, lookback: int = 20):
        self.rsi_period = rsi_period
        self.lookback = lookback
        self.prices: pd.Series = pd.Series(dtype=float)

    def set_data(self, prices: pd.Series) -> None:
        self.prices = prices

    def calculate_rsi(self) -> pd.Series:
        delta = self.prices.diff()
        gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
        rs = gain / loss
        return 100 - (100 / (1 + rs))

    def find_divergence(self) -> Dict:
        rsi = self.calculate_rsi()
        prices = self.prices[-self.lookback:]
        rsi_recent = rsi[-self.lookback:]

        price_low_idx = prices.idxmin()
        # ... rest of implementation

        # Bearish divergence check
        if current_price > prices[price_high_idx] and current_rsi < rsi_at_price_high:
            return {
                'divergence': 'bearish',
                'signal': 'sell',
                ...
            }

Exercise 3: Carry Portfolio Builder (Guided)

Complete the CarryPortfolio class that builds a diversified carry trade portfolio.

class CarryPortfolio:
    """Build diversified carry trade portfolio."""
    
    def __init__(self, max_positions: int = 5, min_carry: float = 2.0):
        self.max_positions = max_positions
        self.min_carry = min_carry
        self.rates: Dict[str, float] = {}
        self.positions: List[Dict] = []
    
    def set_rate(self, currency: str, rate: float) -> None:
        """Set interest rate."""
        self.rates[currency] = rate
    
    def get_all_pairs(self) -> List[Dict]:
        """Get all possible carry pairs."""
        pairs = []
        currencies = list(self.rates.keys())
        
        for i, c1 in enumerate(currencies):
            for c2 in currencies[i+1:]:
                carry = self.rates[c1] - self.rates[c2]
                if abs(carry) >= self.min_carry:
                    if carry > 0:
                        pairs.append({'long': c1, 'short': c2, 'carry': carry})
                    else:
                        pairs.append({'long': c2, 'short': c1, 'carry': ______(carry)})
        
        return sorted(pairs, key=lambda x: x['carry'], reverse=True)
    
    def build_portfolio(self) -> List[Dict]:
        """Build diversified portfolio."""
        all_pairs = self.get_all_pairs()
        portfolio = []
        used_currencies = set()
        
        for pair in all_pairs:
            if len(portfolio) >= self.max_positions:
                break
            
            # Check if currencies already used (for diversification)
            if pair['long'] not in used_currencies or pair['______'] not in used_currencies:
                portfolio.append(pair)
                used_currencies.add(pair['long'])
                used_currencies.add(pair['short'])
        
        self.positions = portfolio
        return portfolio
    
    def portfolio_carry(self) -> float:
        """Calculate total portfolio carry."""
        if not self.positions:
            self.build_portfolio()
        return sum(p['carry'] for p in self.positions) / len(self.positions) if self.positions else 0

# Test
portfolio = CarryPortfolio(max_positions=3, min_carry=2.0)
portfolio.set_rate('USD', 5.25)
portfolio.set_rate('EUR', 4.50)
portfolio.set_rate('JPY', 0.10)
portfolio.set_rate('AUD', 4.35)
portfolio.set_rate('CHF', 1.75)

print("Carry Portfolio:")
for pos in portfolio.build_portfolio():
    print(f"  Long {pos['long']}/Short {pos['short']}: {pos['carry']:.2f}%")
print(f"\nAverage Carry: {portfolio.portfolio_carry():.2f}%")
Solution 3
class CarryPortfolio:
    def __init__(self, max_positions: int = 5, min_carry: float = 2.0):
        self.max_positions = max_positions
        self.min_carry = min_carry
        self.rates: Dict[str, float] = {}
        self.positions: List[Dict] = []

    def set_rate(self, currency: str, rate: float) -> None:
        self.rates[currency] = rate

    def get_all_pairs(self) -> List[Dict]:
        pairs = []
        currencies = list(self.rates.keys())

        for i, c1 in enumerate(currencies):
            for c2 in currencies[i+1:]:
                carry = self.rates[c1] - self.rates[c2]
                if abs(carry) >= self.min_carry:
                    if carry > 0:
                        pairs.append({'long': c1, 'short': c2, 'carry': carry})
                    else:
                        pairs.append({'long': c2, 'short': c1, 'carry': abs(carry)})

        return sorted(pairs, key=lambda x: x['carry'], reverse=True)

    def build_portfolio(self) -> List[Dict]:
        all_pairs = self.get_all_pairs()
        portfolio = []
        used_currencies = set()

        for pair in all_pairs:
            if len(portfolio) >= self.max_positions:
                break

            if pair['long'] not in used_currencies or pair['short'] not in used_currencies:
                portfolio.append(pair)
                used_currencies.add(pair['long'])
                used_currencies.add(pair['short'])

        self.positions = portfolio
        return portfolio

Exercise 4: Complete Trend Following System (Open-ended)

Build a comprehensive trend following system that: - Uses multiple timeframe confirmation - Includes ADX trend filter - Implements trailing stops - Manages position sizing

# Exercise 4: Complete Trend Following System (Open-ended)
#
# Requirements:
# 1. Create class TrendFollowingSystem
# 2. Multiple MA periods (fast, medium, slow)
# 3. ADX filter (only trade when ADX > threshold)
# 4. ATR-based trailing stop
# 5. Position sizing based on account risk %
# 6. Generate signals with entry, stop, and position size
#
# Your implementation:
Solution 4
class TrendFollowingSystem:
    def __init__(self, fast_ma=10, medium_ma=20, slow_ma=50, adx_threshold=25):
        self.fast_ma = fast_ma
        self.medium_ma = medium_ma
        self.slow_ma = slow_ma
        self.adx_threshold = adx_threshold
        self.prices: pd.DataFrame = pd.DataFrame()

    def set_data(self, high, low, close):
        self.prices = pd.DataFrame({'high': high, 'low': low, 'close': close})

    def calculate_indicators(self):
        df = self.prices.copy()
        df['fast'] = df['close'].rolling(self.fast_ma).mean()
        df['medium'] = df['close'].rolling(self.medium_ma).mean()
        df['slow'] = df['close'].rolling(self.slow_ma).mean()

        # ATR
        tr = pd.concat([df['high']-df['low'], abs(df['high']-df['close'].shift(1)), 
                       abs(df['low']-df['close'].shift(1))], axis=1).max(axis=1)
        df['atr'] = tr.rolling(14).mean()

        # ADX (simplified)
        df['adx'] = 30  # Placeholder
        return df

    def calculate_position_size(self, account: float, risk_pct: float, 
                               stop_pips: float, pip_value: float):
        risk_amount = account * risk_pct / 100
        position = risk_amount / (stop_pips * pip_value)
        return position

    def generate_signal(self, account: float = 10000, risk_pct: float = 1.0):
        df = self.calculate_indicators()
        latest = df.iloc[-1]

        # Check alignment and ADX
        bullish = latest['fast'] > latest['medium'] > latest['slow']
        bearish = latest['fast'] < latest['medium'] < latest['slow']
        trending = latest['adx'] > self.adx_threshold

        if bullish and trending:
            stop = latest['close'] - 2 * latest['atr']
            stop_pips = (latest['close'] - stop) / 0.0001
            size = self.calculate_position_size(account, risk_pct, stop_pips, 10)
            return {'signal': 'buy', 'entry': latest['close'], 'stop': stop, 'size': size}
        elif bearish and trending:
            stop = latest['close'] + 2 * latest['atr']
            stop_pips = (stop - latest['close']) / 0.0001
            size = self.calculate_position_size(account, risk_pct, stop_pips, 10)
            return {'signal': 'sell', 'entry': latest['close'], 'stop': stop, 'size': size}

        return {'signal': 'neutral'}

Exercise 5: Range Trading System (Open-ended)

Build a range trading system that: - Detects ranging markets - Identifies support/resistance levels - Uses oscillators for confirmation - Sets appropriate targets and stops

# Exercise 5: Range Trading System (Open-ended)
#
# Requirements:
# 1. Create class RangeTradingSystem
# 2. Detect ranging markets (low ADX or range detection)
# 3. Identify support/resistance from recent highs/lows
# 4. Use RSI or Stochastic for entry timing
# 5. Target opposite boundary of range
# 6. Stop outside range boundary
#
# Your implementation:
Solution 5
class RangeTradingSystem:
    def __init__(self, lookback=20, rsi_period=14):
        self.lookback = lookback
        self.rsi_period = rsi_period
        self.prices: pd.DataFrame = pd.DataFrame()

    def set_data(self, high, low, close):
        self.prices = pd.DataFrame({'high': high, 'low': low, 'close': close})

    def detect_range(self):
        resistance = self.prices['high'].rolling(self.lookback).max().iloc[-1]
        support = self.prices['low'].rolling(self.lookback).min().iloc[-1]
        range_size = resistance - support

        # Check if price stayed in range
        recent = self.prices[-self.lookback:]
        in_range = all((recent['high'] <= resistance * 1.01) & (recent['low'] >= support * 0.99))

        return {'ranging': in_range, 'resistance': resistance, 'support': support, 'range': range_size}

    def calculate_rsi(self):
        delta = self.prices['close'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
        return 100 - (100 / (1 + gain/loss))

    def generate_signal(self):
        rng = self.detect_range()
        if not rng['ranging']:
            return {'signal': 'no_trade', 'reason': 'Not ranging'}

        rsi = self.calculate_rsi().iloc[-1]
        price = self.prices['close'].iloc[-1]

        near_support = (price - rng['support']) / rng['range'] < 0.2
        near_resistance = (rng['resistance'] - price) / rng['range'] < 0.2

        if near_support and rsi < 30:
            return {
                'signal': 'buy',
                'entry': price,
                'target': rng['resistance'],
                'stop': rng['support'] - rng['range'] * 0.1
            }
        elif near_resistance and rsi > 70:
            return {
                'signal': 'sell',
                'entry': price,
                'target': rng['support'],
                'stop': rng['resistance'] + rng['range'] * 0.1
            }

        return {'signal': 'wait', 'reason': 'Not at range extremes'}

Exercise 6: Multi-Strategy Manager (Open-ended)

Build a strategy manager that: - Combines trend, range, carry, and news strategies - Allocates capital based on market regime - Manages overall portfolio risk - Generates consolidated signals

# Exercise 6: Multi-Strategy Manager (Open-ended)
#
# Requirements:
# 1. Create class MultiStrategyManager
# 2. Include: trend, range, carry, news strategies
# 3. Detect market regime (trending vs ranging)
# 4. Allocate capital based on regime
# 5. Set overall portfolio risk limits
# 6. Generate consolidated trade recommendations
#
# Your implementation:
Solution 6
class MultiStrategyManager:
    def __init__(self, total_capital: float, max_risk_pct: float = 5.0):
        self.capital = total_capital
        self.max_risk = max_risk_pct
        self.strategies = {}
        self.allocations = {'trend': 0.3, 'range': 0.3, 'carry': 0.3, 'news': 0.1}

    def add_strategy(self, name: str, strategy) -> None:
        self.strategies[name] = strategy

    def detect_regime(self, adx: float) -> str:
        if adx > 25:
            return 'trending'
        return 'ranging'

    def adjust_allocations(self, regime: str) -> None:
        if regime == 'trending':
            self.allocations = {'trend': 0.5, 'range': 0.1, 'carry': 0.3, 'news': 0.1}
        else:
            self.allocations = {'trend': 0.1, 'range': 0.5, 'carry': 0.3, 'news': 0.1}

    def get_strategy_capital(self, strategy_name: str) -> float:
        return self.capital * self.allocations.get(strategy_name, 0)

    def generate_signals(self, regime: str = 'trending') -> List[Dict]:
        self.adjust_allocations(regime)
        all_signals = []

        for name, strategy in self.strategies.items():
            if hasattr(strategy, 'generate_signal'):
                signal = strategy.generate_signal()
                signal['strategy'] = name
                signal['capital_allocated'] = self.get_strategy_capital(name)
                all_signals.append(signal)

        return [s for s in all_signals if s.get('signal') not in ['neutral', 'no_trade']]

    def check_risk_limits(self, signals: List[Dict]) -> List[Dict]:
        total_risk = sum(s.get('risk_pct', 1) for s in signals)

        if total_risk > self.max_risk:
            scale = self.max_risk / total_risk
            for s in signals:
                s['position_scale'] = scale

        return signals

Module Project: Multi-Strategy Forex System

Build a production-ready system that combines all trading strategies.

class ForexTradingSystem:
    """
    Production-ready multi-strategy forex trading system.
    
    Integrates: Trend following, Mean reversion, Carry trade, News trading.
    Features: Regime detection, Capital allocation, Risk management.
    """
    
    def __init__(self, capital: float = 100000, max_risk_pct: float = 2.0):
        self.capital = capital
        self.max_risk_pct = max_risk_pct
        
        # Initialize strategies
        self.ma_strategy = MovingAverageCrossover()
        self.mr_strategy = MeanReversionStrategy()
        self.carry_strategy = CarryTradeStrategy()
        
        # Market data
        self.prices: Dict[str, pd.Series] = {}
        self.rates: Dict[str, float] = {}
    
    def load_price_data(self, pair: str, prices: pd.Series) -> None:
        """Load price data for a currency pair."""
        self.prices[pair] = prices
    
    def set_interest_rate(self, currency: str, rate: float) -> None:
        """Set interest rate for carry calculations."""
        self.rates[currency] = rate
        self.carry_strategy.set_rate(currency, rate)
    
    def detect_regime(self, pair: str) -> str:
        """Detect market regime (trending vs ranging)."""
        if pair not in self.prices:
            return 'unknown'
        
        prices = self.prices[pair]
        
        # Simple regime detection using MA slope and range
        ma20 = prices.rolling(20).mean()
        ma_slope = (ma20.iloc[-1] - ma20.iloc[-10]) / ma20.iloc[-10] * 100
        
        range_pct = (prices.rolling(20).max().iloc[-1] - prices.rolling(20).min().iloc[-1]) / prices.iloc[-1] * 100
        
        if abs(ma_slope) > 1 and range_pct > 2:
            return 'trending'
        elif range_pct < 1.5:
            return 'ranging'
        return 'transitioning'
    
    def get_trend_signals(self, pair: str) -> Dict:
        """Get trend following signals."""
        if pair not in self.prices:
            return {'signal': 'no_data'}
        
        self.ma_strategy.set_data(self.prices[pair])
        return self.ma_strategy.get_current_signal()
    
    def get_mr_signals(self, pair: str) -> Dict:
        """Get mean reversion signals."""
        if pair not in self.prices:
            return {'signal': 'no_data'}
        
        self.mr_strategy.set_data(self.prices[pair])
        return self.mr_strategy.get_current_signal()
    
    def get_carry_signals(self) -> List[Dict]:
        """Get carry trade signals."""
        self.carry_strategy.set_vix(16)  # Default low vol
        return self.carry_strategy.generate_signals()
    
    def generate_recommendations(self, pair: str) -> List[Dict]:
        """Generate all trading recommendations."""
        recommendations = []
        regime = self.detect_regime(pair)
        
        # Trend following (prioritize in trending regime)
        trend_signal = self.get_trend_signals(pair)
        if trend_signal.get('signal') in ['long', 'short']:
            recommendations.append({
                'strategy': 'trend_following',
                'pair': pair,
                'direction': trend_signal['signal'],
                'confidence': 'high' if regime == 'trending' else 'low',
                'details': trend_signal
            })
        
        # Mean reversion (prioritize in ranging regime)
        mr_signal = self.get_mr_signals(pair)
        if mr_signal.get('signal') in ['buy', 'sell']:
            recommendations.append({
                'strategy': 'mean_reversion',
                'pair': pair,
                'direction': 'long' if mr_signal['signal'] == 'buy' else 'short',
                'confidence': 'high' if regime == 'ranging' else 'low',
                'details': mr_signal
            })
        
        # Carry trades
        for carry in self.get_carry_signals():
            if carry.get('signal') == 'carry_long':
                recommendations.append({
                    'strategy': 'carry_trade',
                    'pair': carry.get('pair'),
                    'direction': 'long',
                    'confidence': carry.get('confidence', 'medium'),
                    'details': carry
                })
        
        return recommendations
    
    def calculate_position_size(self, pair: str, stop_pips: float) -> float:
        """Calculate position size based on risk."""
        risk_amount = self.capital * self.max_risk_pct / 100
        pip_value = 10  # Standard lot pip value for major pairs
        position_size = risk_amount / (stop_pips * pip_value)
        return round(position_size, 2)
    
    def print_dashboard(self, pair: str) -> None:
        """Print trading dashboard."""
        print("\n" + "=" * 60)
        print("  FOREX TRADING SYSTEM DASHBOARD")
        print(f"  {datetime.now().strftime('%Y-%m-%d %H:%M')}")
        print("=" * 60)
        
        # Market Regime
        regime = self.detect_regime(pair)
        print(f"\nMarket Regime: {regime.upper()}")
        print(f"Primary Pair: {pair}")
        
        # Strategy Signals
        print("\n" + "-" * 40)
        print("STRATEGY SIGNALS")
        print("-" * 40)
        
        print(f"\nTrend Following: {self.get_trend_signals(pair).get('signal', 'N/A')}")
        print(f"Mean Reversion: {self.get_mr_signals(pair).get('signal', 'N/A')}")
        
        carry_signals = self.get_carry_signals()
        print(f"Carry Trades: {len(carry_signals)} opportunities")
        
        # Recommendations
        print("\n" + "-" * 40)
        print("RECOMMENDATIONS")
        print("-" * 40)
        
        for i, rec in enumerate(self.generate_recommendations(pair), 1):
            print(f"\n{i}. {rec['strategy'].upper()}")
            print(f"   Pair: {rec['pair']} | Direction: {rec['direction'].upper()}")
            print(f"   Confidence: {rec['confidence']}")
        
        print("\n" + "=" * 60)
# Demo the system
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=100, freq='D')

system = ForexTradingSystem(capital=100000, max_risk_pct=1.0)

# Load price data
eurusd = pd.Series(1.0800 + np.cumsum(np.random.normal(0.0002, 0.005, 100)), index=dates)
usdjpy = pd.Series(150.00 + np.cumsum(np.random.normal(0.02, 0.5, 100)), index=dates)

system.load_price_data('EURUSD', eurusd)
system.load_price_data('USDJPY', usdjpy)

# Set interest rates
system.set_interest_rate('USD', 5.25)
system.set_interest_rate('EUR', 4.50)
system.set_interest_rate('JPY', 0.10)
system.set_interest_rate('AUD', 4.35)

# Set volatilities for carry strategy
system.carry_strategy.set_volatility('USDJPY', 9.0)
system.carry_strategy.set_volatility('AUDJPY', 11.0)

# Print dashboard
system.print_dashboard('EURUSD')

Key Takeaways

  • Trend Following: MA crossovers and breakouts work best in trending markets; use ADX filter
  • Mean Reversion: Range trading and Bollinger Band strategies work in low-volatility, ranging markets
  • Carry Trade: Profit from interest rate differentials; works best in risk-on, low-volatility environments
  • News Trading: Straddle for unknown direction, fade extended spikes; requires fast execution
  • Regime Detection: Match strategy to market conditions; trend strategies fail in ranges and vice versa
  • Position Sizing: Always size based on account risk %, not potential profit
  • Multi-Strategy: Diversify across strategies that perform differently in various conditions
  • Risk First: No strategy works all the time; proper risk management is essential

Next: Part 3 - Risk & Execution where we'll build comprehensive risk management and backtesting systems.

Module 9: Risk Management

Part 3: Risk & Execution

Duration Exercises
~2.5 hours 6

Learning Objectives

  • Implement pip-based and ATR-based stop losses for forex
  • Calculate tick-based risk and position sizing for futures
  • Manage portfolio-level currency exposure and hedging
  • Handle weekend gaps and overnight risk

Prerequisites

  • Modules 1-8 (Forex/Futures fundamentals, strategies)
  • Understanding of leverage and margin
  • Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')

9.1 Forex Risk Management

Forex risk management requires understanding pip values, proper stop placement, and currency correlation risks.

Pip Value Calculation

Pair Type Pip Size Pip Value (Standard Lot)
XXX/USD 0.0001 $10
USD/XXX 0.0001 $10 / exchange rate
XXX/JPY 0.01 ~$7-9 (varies)
Cross pairs 0.0001 Requires conversion
class ForexRiskCalculator:
    """Calculate forex position risk and sizing."""
    
    # Pip sizes by pair type
    PIP_SIZES = {
        'standard': 0.0001,  # Most pairs
        'jpy': 0.01,  # JPY pairs
    }
    
    def __init__(self, account_currency: str = 'USD'):
        self.account_currency = account_currency
        self.exchange_rates: Dict[str, float] = {}  # For conversion
    
    def set_exchange_rate(self, pair: str, rate: float) -> None:
        """Set exchange rate for conversion."""
        self.exchange_rates[pair] = rate
    
    def get_pip_size(self, pair: str) -> float:
        """Get pip size for a currency pair."""
        if 'JPY' in pair:
            return self.PIP_SIZES['jpy']
        return self.PIP_SIZES['standard']
    
    def calculate_pip_value(self, pair: str, lot_size: float = 1.0) -> float:
        """Calculate pip value in account currency."""
        pip_size = self.get_pip_size(pair)
        units = lot_size * 100000  # Standard lot = 100,000 units
        
        # Base pip value
        pip_value = pip_size * units
        
        # Convert to account currency
        quote_currency = pair[-3:]
        
        if quote_currency == self.account_currency:
            return pip_value
        elif quote_currency == 'JPY' and self.account_currency == 'USD':
            usdjpy = self.exchange_rates.get('USDJPY', 150.0)
            return pip_value / usdjpy
        else:
            # Try to find conversion rate
            conversion_pair = f"{quote_currency}{self.account_currency}"
            if conversion_pair in self.exchange_rates:
                return pip_value * self.exchange_rates[conversion_pair]
            
            reverse_pair = f"{self.account_currency}{quote_currency}"
            if reverse_pair in self.exchange_rates:
                return pip_value / self.exchange_rates[reverse_pair]
        
        return pip_value  # Default
    
    def calculate_position_size(self, account_balance: float, risk_percent: float,
                               stop_loss_pips: float, pair: str) -> Dict:
        """Calculate position size based on risk."""
        risk_amount = account_balance * (risk_percent / 100)
        pip_value_per_lot = self.calculate_pip_value(pair, 1.0)
        
        # Position size in lots
        position_lots = risk_amount / (stop_loss_pips * pip_value_per_lot)
        
        # Convert to units
        position_units = position_lots * 100000
        
        return {
            'pair': pair,
            'account_balance': account_balance,
            'risk_percent': risk_percent,
            'risk_amount': risk_amount,
            'stop_loss_pips': stop_loss_pips,
            'pip_value_per_lot': pip_value_per_lot,
            'position_lots': round(position_lots, 2),
            'position_units': int(position_units),
            'mini_lots': round(position_lots * 10, 1),
            'micro_lots': round(position_lots * 100, 0)
        }
    
    def calculate_risk_from_position(self, pair: str, position_lots: float,
                                    stop_loss_pips: float, account_balance: float) -> Dict:
        """Calculate risk from an existing position."""
        pip_value = self.calculate_pip_value(pair, position_lots)
        risk_amount = stop_loss_pips * pip_value
        risk_percent = (risk_amount / account_balance) * 100
        
        return {
            'pair': pair,
            'position_lots': position_lots,
            'stop_loss_pips': stop_loss_pips,
            'risk_amount': risk_amount,
            'risk_percent': risk_percent,
            'acceptable': risk_percent <= 2.0
        }

# Demo
forex_risk = ForexRiskCalculator(account_currency='USD')
forex_risk.set_exchange_rate('USDJPY', 150.50)
forex_risk.set_exchange_rate('EURUSD', 1.0850)

print("Pip Values (per standard lot):")
print(f"  EURUSD: ${forex_risk.calculate_pip_value('EURUSD', 1.0):.2f}")
print(f"  USDJPY: ${forex_risk.calculate_pip_value('USDJPY', 1.0):.2f}")

print("\nPosition Sizing (1% risk, 30 pip stop):")
sizing = forex_risk.calculate_position_size(
    account_balance=10000,
    risk_percent=1.0,
    stop_loss_pips=30,
    pair='EURUSD'
)
print(f"  Risk Amount: ${sizing['risk_amount']:.2f}")
print(f"  Position Size: {sizing['position_lots']:.2f} lots ({sizing['position_units']:,} units)")
class StopLossCalculator:
    """Calculate stop loss levels using various methods."""
    
    def __init__(self):
        self.prices: pd.DataFrame = pd.DataFrame()
    
    def set_data(self, high: pd.Series, low: pd.Series, close: pd.Series) -> None:
        """Set OHLC data."""
        self.prices = pd.DataFrame({'high': high, 'low': low, 'close': close})
    
    def fixed_pip_stop(self, entry: float, direction: str, 
                       stop_pips: float, pip_size: float = 0.0001) -> float:
        """Calculate fixed pip-based stop loss."""
        stop_distance = stop_pips * pip_size
        
        if direction == 'long':
            return entry - stop_distance
        else:
            return entry + stop_distance
    
    def calculate_atr(self, period: int = 14) -> pd.Series:
        """Calculate Average True Range."""
        high = self.prices['high']
        low = self.prices['low']
        close = self.prices['close']
        
        tr1 = high - low
        tr2 = abs(high - close.shift(1))
        tr3 = abs(low - close.shift(1))
        
        tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
        return tr.rolling(period).mean()
    
    def atr_stop(self, entry: float, direction: str, 
                 atr_multiplier: float = 2.0, atr_period: int = 14) -> Dict:
        """Calculate ATR-based stop loss."""
        atr = self.calculate_atr(atr_period)
        current_atr = atr.iloc[-1]
        stop_distance = current_atr * atr_multiplier
        
        if direction == 'long':
            stop_price = entry - stop_distance
        else:
            stop_price = entry + stop_distance
        
        return {
            'stop_price': stop_price,
            'atr': current_atr,
            'stop_distance': stop_distance,
            'multiplier': atr_multiplier
        }
    
    def swing_stop(self, direction: str, lookback: int = 10) -> Dict:
        """Calculate stop based on recent swing high/low."""
        recent = self.prices.tail(lookback)
        
        if direction == 'long':
            swing_low = recent['low'].min()
            swing_idx = recent['low'].idxmin()
            stop_price = swing_low
        else:
            swing_high = recent['high'].max()
            swing_idx = recent['high'].idxmax()
            stop_price = swing_high
        
        return {
            'stop_price': stop_price,
            'swing_date': swing_idx,
            'lookback': lookback
        }
    
    def trailing_stop(self, entry: float, current_price: float,
                     direction: str, trail_pips: float,
                     current_stop: float = None,
                     pip_size: float = 0.0001) -> Dict:
        """Calculate trailing stop level."""
        trail_distance = trail_pips * pip_size
        
        if direction == 'long':
            new_stop = current_price - trail_distance
            if current_stop is not None:
                new_stop = max(new_stop, current_stop)
            triggered = current_price <= new_stop
        else:
            new_stop = current_price + trail_distance
            if current_stop is not None:
                new_stop = min(new_stop, current_stop)
            triggered = current_price >= new_stop
        
        profit_pips = abs(current_price - entry) / pip_size
        
        return {
            'stop_price': new_stop,
            'current_price': current_price,
            'profit_pips': profit_pips,
            'triggered': triggered
        }

# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')
close = pd.Series(1.0800 + np.cumsum(np.random.normal(0.0002, 0.005, 60)), index=dates)
high = close + np.random.uniform(0.001, 0.003, 60)
low = close - np.random.uniform(0.001, 0.003, 60)

stop_calc = StopLossCalculator()
stop_calc.set_data(high, low, close)

entry = 1.0900
print(f"Entry Price: {entry}")
print(f"\nFixed 30-pip stop (long): {stop_calc.fixed_pip_stop(entry, 'long', 30):.5f}")
print(f"\nATR Stop (2x): {stop_calc.atr_stop(entry, 'long', 2.0)}")
print(f"\nSwing Stop: {stop_calc.swing_stop('long', 10)}")

9.2 Futures Risk Management

Futures risk is calculated based on tick values, contract specifications, and notional exposure.

Common Futures Contract Specifications

Contract Symbol Tick Size Tick Value Margin
E-mini S&P 500 ES 0.25 $12.50 ~$13,200
E-mini Nasdaq NQ 0.25 $5.00 ~$17,600
Crude Oil CL 0.01 $10.00 ~$6,500
Gold GC 0.10 $10.00 ~$9,000
Euro FX 6E 0.0001 $12.50 ~$2,500
@dataclass
class FuturesContract:
    """Futures contract specifications."""
    symbol: str
    name: str
    tick_size: float
    tick_value: float
    contract_size: float
    initial_margin: float
    maintenance_margin: float
    
    @property
    def point_value(self) -> float:
        """Value of 1 full point move."""
        return self.tick_value / self.tick_size


class FuturesRiskCalculator:
    """Calculate futures position risk and sizing."""
    
    # Standard contract specs
    CONTRACTS = {
        'ES': FuturesContract('ES', 'E-mini S&P 500', 0.25, 12.50, 50, 13200, 12000),
        'NQ': FuturesContract('NQ', 'E-mini Nasdaq', 0.25, 5.00, 20, 17600, 16000),
        'CL': FuturesContract('CL', 'Crude Oil', 0.01, 10.00, 1000, 6500, 5900),
        'GC': FuturesContract('GC', 'Gold', 0.10, 10.00, 100, 9000, 8200),
        '6E': FuturesContract('6E', 'Euro FX', 0.0001, 12.50, 125000, 2500, 2275),
        'ZB': FuturesContract('ZB', 'T-Bond', 1/32, 31.25, 100000, 4400, 4000),
    }
    
    def __init__(self):
        self.positions: List[Dict] = []
    
    def get_contract(self, symbol: str) -> Optional[FuturesContract]:
        """Get contract specifications."""
        return self.CONTRACTS.get(symbol)
    
    def calculate_tick_risk(self, symbol: str, stop_ticks: float, 
                           num_contracts: int = 1) -> Dict:
        """Calculate risk based on tick distance."""
        contract = self.get_contract(symbol)
        if not contract:
            return {'error': f'Unknown contract: {symbol}'}
        
        risk_per_contract = stop_ticks * contract.tick_value
        total_risk = risk_per_contract * num_contracts
        
        return {
            'symbol': symbol,
            'contracts': num_contracts,
            'stop_ticks': stop_ticks,
            'risk_per_contract': risk_per_contract,
            'total_risk': total_risk,
            'tick_value': contract.tick_value
        }
    
    def calculate_point_risk(self, symbol: str, entry: float, stop: float,
                            num_contracts: int = 1) -> Dict:
        """Calculate risk based on price levels."""
        contract = self.get_contract(symbol)
        if not contract:
            return {'error': f'Unknown contract: {symbol}'}
        
        point_distance = abs(entry - stop)
        tick_distance = point_distance / contract.tick_size
        
        return self.calculate_tick_risk(symbol, tick_distance, num_contracts)
    
    def calculate_position_size(self, symbol: str, account_balance: float,
                               risk_percent: float, stop_ticks: float) -> Dict:
        """Calculate position size based on risk."""
        contract = self.get_contract(symbol)
        if not contract:
            return {'error': f'Unknown contract: {symbol}'}
        
        risk_amount = account_balance * (risk_percent / 100)
        risk_per_contract = stop_ticks * contract.tick_value
        
        max_contracts = int(risk_amount / risk_per_contract)
        
        # Check margin requirements
        margin_required = max_contracts * contract.initial_margin
        margin_limited = margin_required > account_balance * 0.5  # Use max 50% for margin
        
        if margin_limited:
            margin_contracts = int((account_balance * 0.5) / contract.initial_margin)
            max_contracts = min(max_contracts, margin_contracts)
        
        return {
            'symbol': symbol,
            'account_balance': account_balance,
            'risk_percent': risk_percent,
            'risk_amount': risk_amount,
            'stop_ticks': stop_ticks,
            'risk_per_contract': risk_per_contract,
            'max_contracts': max_contracts,
            'margin_required': max_contracts * contract.initial_margin,
            'margin_limited': margin_limited
        }
    
    def calculate_notional_value(self, symbol: str, price: float,
                                num_contracts: int = 1) -> float:
        """Calculate notional value of position."""
        contract = self.get_contract(symbol)
        if not contract:
            return 0.0
        
        return price * contract.contract_size * num_contracts
    
    def calculate_leverage(self, symbol: str, price: float,
                          num_contracts: int, account_balance: float) -> float:
        """Calculate effective leverage."""
        notional = self.calculate_notional_value(symbol, price, num_contracts)
        return notional / account_balance if account_balance > 0 else 0

# Demo
futures_risk = FuturesRiskCalculator()

print("Futures Risk Calculations:")
print("\nES (E-mini S&P 500):")
es_risk = futures_risk.calculate_tick_risk('ES', stop_ticks=40, num_contracts=2)
print(f"  40 tick stop with 2 contracts: ${es_risk['total_risk']:.2f} risk")

print("\nPosition Sizing (1% risk, 20 tick stop):")
sizing = futures_risk.calculate_position_size('ES', account_balance=50000, 
                                              risk_percent=1.0, stop_ticks=20)
print(f"  Max Contracts: {sizing['max_contracts']}")
print(f"  Margin Required: ${sizing['margin_required']:,.2f}")

print("\nNotional & Leverage:")
notional = futures_risk.calculate_notional_value('ES', 5000, 2)
leverage = futures_risk.calculate_leverage('ES', 5000, 2, 50000)
print(f"  Notional Value (2 ES @ 5000): ${notional:,.2f}")
print(f"  Effective Leverage: {leverage:.1f}x")

9.3 Portfolio Risk

Managing portfolio-level risk requires understanding currency exposure, correlations, and hedging strategies.

class CurrencyExposureCalculator:
    """Calculate and manage currency exposure across portfolio."""
    
    def __init__(self, base_currency: str = 'USD'):
        self.base_currency = base_currency
        self.positions: List[Dict] = []
    
    def add_position(self, pair: str, direction: str, 
                    units: float, entry_price: float) -> None:
        """Add a forex position."""
        base = pair[:3]
        quote = pair[3:]
        
        # Calculate exposure
        if direction == 'long':
            base_exposure = units
            quote_exposure = -units * entry_price
        else:
            base_exposure = -units
            quote_exposure = units * entry_price
        
        self.positions.append({
            'pair': pair,
            'direction': direction,
            'units': units,
            'entry_price': entry_price,
            'base_currency': base,
            'quote_currency': quote,
            'base_exposure': base_exposure,
            'quote_exposure': quote_exposure
        })
    
    def get_net_exposure(self) -> Dict[str, float]:
        """Calculate net exposure by currency."""
        exposure = {}
        
        for pos in self.positions:
            base = pos['base_currency']
            quote = pos['quote_currency']
            
            exposure[base] = exposure.get(base, 0) + pos['base_exposure']
            exposure[quote] = exposure.get(quote, 0) + pos['quote_exposure']
        
        return exposure
    
    def check_concentration(self, max_single_currency_pct: float = 30) -> Dict:
        """Check for currency concentration risk."""
        exposure = self.get_net_exposure()
        total_exposure = sum(abs(v) for v in exposure.values())
        
        if total_exposure == 0:
            return {'concentrated': False, 'details': {}}
        
        concentration = {}
        warnings = []
        
        for currency, exp in exposure.items():
            pct = abs(exp) / total_exposure * 100
            concentration[currency] = pct
            
            if pct > max_single_currency_pct:
                warnings.append(f"{currency}: {pct:.1f}% (max {max_single_currency_pct}%)")
        
        return {
            'concentrated': len(warnings) > 0,
            'warnings': warnings,
            'concentration': concentration
        }
    
    def suggest_hedge(self, currency: str, hedge_pct: float = 50) -> Dict:
        """Suggest hedge for a currency exposure."""
        exposure = self.get_net_exposure()
        current_exposure = exposure.get(currency, 0)
        
        if current_exposure == 0:
            return {'hedge_needed': False}
        
        hedge_amount = abs(current_exposure) * (hedge_pct / 100)
        
        # Determine hedge direction
        if current_exposure > 0:
            hedge_direction = 'sell'
        else:
            hedge_direction = 'buy'
        
        # Suggest hedge pair (vs USD)
        if currency != self.base_currency:
            hedge_pair = f"{currency}{self.base_currency}"
        else:
            hedge_pair = 'N/A (base currency)'
        
        return {
            'hedge_needed': True,
            'currency': currency,
            'current_exposure': current_exposure,
            'hedge_pair': hedge_pair,
            'hedge_direction': hedge_direction,
            'hedge_amount': hedge_amount,
            'remaining_exposure': current_exposure - (hedge_amount if current_exposure > 0 else -hedge_amount)
        }

# Demo
exposure_calc = CurrencyExposureCalculator()

# Add positions
exposure_calc.add_position('EURUSD', 'long', 100000, 1.0850)
exposure_calc.add_position('GBPUSD', 'long', 50000, 1.2650)
exposure_calc.add_position('USDJPY', 'long', 100000, 150.50)
exposure_calc.add_position('EURJPY', 'short', 50000, 163.30)

print("Net Currency Exposure:")
for curr, exp in exposure_calc.get_net_exposure().items():
    print(f"  {curr}: {exp:,.0f}")

print("\nConcentration Check:")
conc = exposure_calc.check_concentration(30)
print(f"  Concentrated: {conc['concentrated']}")
if conc['warnings']:
    for w in conc['warnings']:
        print(f"  Warning: {w}")

print("\nHedge Suggestion for EUR:")
hedge = exposure_calc.suggest_hedge('EUR', 50)
print(f"  {hedge}")
class CorrelationRiskManager:
    """Manage risk from correlated positions."""
    
    # Known high correlations between pairs
    KNOWN_CORRELATIONS = {
        ('EURUSD', 'GBPUSD'): 0.85,
        ('AUDUSD', 'NZDUSD'): 0.90,
        ('EURUSD', 'USDCHF'): -0.90,
        ('USDJPY', 'EURJPY'): 0.75,
        ('AUDUSD', 'USDCAD'): -0.70,
    }
    
    def __init__(self):
        self.positions: Dict[str, Dict] = {}
        self.price_data: Dict[str, pd.Series] = {}
    
    def add_position(self, pair: str, direction: str, risk_amount: float) -> None:
        """Add position with its risk."""
        self.positions[pair] = {
            'direction': direction,
            'risk': risk_amount
        }
    
    def add_price_data(self, pair: str, prices: pd.Series) -> None:
        """Add price data for correlation calculation."""
        self.price_data[pair] = prices
    
    def get_correlation(self, pair1: str, pair2: str) -> float:
        """Get correlation between two pairs."""
        # Check known correlations
        key = (pair1, pair2) if (pair1, pair2) in self.KNOWN_CORRELATIONS else (pair2, pair1)
        if key in self.KNOWN_CORRELATIONS:
            return self.KNOWN_CORRELATIONS[key]
        
        # Calculate from price data
        if pair1 in self.price_data and pair2 in self.price_data:
            ret1 = self.price_data[pair1].pct_change()
            ret2 = self.price_data[pair2].pct_change()
            return ret1.corr(ret2)
        
        return 0.0
    
    def calculate_correlated_risk(self) -> Dict:
        """Calculate portfolio risk considering correlations."""
        pairs = list(self.positions.keys())
        n = len(pairs)
        
        if n == 0:
            return {'total_risk': 0, 'correlated_risk': 0}
        
        # Simple risk (sum of individual risks)
        simple_risk = sum(pos['risk'] for pos in self.positions.values())
        
        # Correlated risk (using correlation adjustment)
        # For same direction positions with high correlation: risk increases
        # For opposite direction positions with high correlation: risk decreases (hedge)
        
        correlated_risk_squared = 0
        correlation_adjustments = []
        
        for i, pair1 in enumerate(pairs):
            pos1 = self.positions[pair1]
            risk1 = pos1['risk']
            dir1 = 1 if pos1['direction'] == 'long' else -1
            
            correlated_risk_squared += risk1 ** 2
            
            for pair2 in pairs[i+1:]:
                pos2 = self.positions[pair2]
                risk2 = pos2['risk']
                dir2 = 1 if pos2['direction'] == 'long' else -1
                
                corr = self.get_correlation(pair1, pair2)
                
                # Adjust for direction
                effective_corr = corr * dir1 * dir2
                
                # Add correlation contribution
                correlated_risk_squared += 2 * risk1 * risk2 * effective_corr
                
                if abs(corr) > 0.6:
                    correlation_adjustments.append({
                        'pair1': pair1,
                        'pair2': pair2,
                        'correlation': corr,
                        'effective_correlation': effective_corr,
                        'impact': 'adds_risk' if effective_corr > 0 else 'hedges'
                    })
        
        correlated_risk = np.sqrt(max(correlated_risk_squared, 0))
        
        return {
            'simple_risk': simple_risk,
            'correlated_risk': correlated_risk,
            'diversification_benefit': simple_risk - correlated_risk,
            'correlation_adjustments': correlation_adjustments
        }

# Demo
corr_manager = CorrelationRiskManager()

# Add positions (same direction on correlated pairs = more risk)
corr_manager.add_position('EURUSD', 'long', 500)
corr_manager.add_position('GBPUSD', 'long', 500)  # High correlation with EUR
corr_manager.add_position('USDCHF', 'long', 300)  # Negative correlation (hedge)

print("Correlation Risk Analysis:")
risk = corr_manager.calculate_correlated_risk()
print(f"  Simple Risk (sum): ${risk['simple_risk']:.2f}")
print(f"  Correlated Risk: ${risk['correlated_risk']:.2f}")
print(f"  Diversification Benefit: ${risk['diversification_benefit']:.2f}")
print("\nCorrelation Impacts:")
for adj in risk['correlation_adjustments']:
    print(f"  {adj['pair1']}/{adj['pair2']}: {adj['correlation']:.2f} -> {adj['impact']}")

9.4 Weekend & Gap Risk

Forex markets close on weekends, creating gap risk that must be managed.

class GapRiskManager:
    """Manage weekend and holiday gap risk."""
    
    # Historical average gap sizes (pips)
    TYPICAL_GAP_SIZES = {
        'EURUSD': 15,
        'GBPUSD': 20,
        'USDJPY': 25,
        'AUDUSD': 20,
        'USDCAD': 15,
    }
    
    # Major events that increase gap risk
    HIGH_RISK_EVENTS = [
        'G7_meeting',
        'central_bank_emergency',
        'geopolitical_crisis',
        'election',
    ]
    
    def __init__(self):
        self.positions: List[Dict] = []
        self.upcoming_events: List[str] = []
    
    def add_position(self, pair: str, direction: str, 
                    units: float, current_pnl_pips: float) -> None:
        """Add position for gap risk analysis."""
        self.positions.append({
            'pair': pair,
            'direction': direction,
            'units': units,
            'current_pnl_pips': current_pnl_pips
        })
    
    def set_upcoming_events(self, events: List[str]) -> None:
        """Set upcoming high-impact events."""
        self.upcoming_events = events
    
    def calculate_gap_risk(self) -> Dict:
        """Calculate potential gap risk for all positions."""
        total_gap_risk = 0
        position_risks = []
        
        # Multiplier for high-risk events
        event_multiplier = 1.0
        if any(e in self.HIGH_RISK_EVENTS for e in self.upcoming_events):
            event_multiplier = 2.0
        
        for pos in self.positions:
            typical_gap = self.TYPICAL_GAP_SIZES.get(pos['pair'], 20)
            worst_case_gap = typical_gap * 3 * event_multiplier
            
            # Calculate potential loss from adverse gap
            pip_value = 10  # Assuming standard lot pip value
            lots = pos['units'] / 100000
            potential_loss = worst_case_gap * pip_value * lots
            
            position_risks.append({
                'pair': pos['pair'],
                'direction': pos['direction'],
                'typical_gap': typical_gap,
                'worst_case_gap': worst_case_gap,
                'potential_loss': potential_loss,
                'current_pnl_pips': pos['current_pnl_pips']
            })
            
            total_gap_risk += potential_loss
        
        return {
            'total_gap_risk': total_gap_risk,
            'event_multiplier': event_multiplier,
            'high_risk_events': self.upcoming_events,
            'position_risks': position_risks
        }
    
    def get_position_reduction_advice(self, account_balance: float,
                                      max_gap_risk_pct: float = 5.0) -> Dict:
        """Advise on position reduction before weekend."""
        gap_risk = self.calculate_gap_risk()
        current_risk_pct = (gap_risk['total_gap_risk'] / account_balance) * 100
        
        if current_risk_pct <= max_gap_risk_pct:
            return {
                'reduce_needed': False,
                'current_risk_pct': current_risk_pct,
                'max_allowed': max_gap_risk_pct
            }
        
        # Calculate reduction needed
        reduction_factor = max_gap_risk_pct / current_risk_pct
        
        recommendations = []
        for pos_risk in gap_risk['position_risks']:
            # Prioritize reducing losing positions
            if pos_risk['current_pnl_pips'] < 0:
                priority = 'high'
                suggested_reduction = 0.75  # Close 75%
            elif pos_risk['current_pnl_pips'] < 20:
                priority = 'medium'
                suggested_reduction = 0.50  # Close 50%
            else:
                priority = 'low'
                suggested_reduction = 1 - reduction_factor
            
            recommendations.append({
                'pair': pos_risk['pair'],
                'priority': priority,
                'suggested_reduction': suggested_reduction,
                'reason': 'losing' if pos_risk['current_pnl_pips'] < 0 else 'gap_risk'
            })
        
        # Sort by priority
        recommendations.sort(key=lambda x: {'high': 0, 'medium': 1, 'low': 2}[x['priority']])
        
        return {
            'reduce_needed': True,
            'current_risk_pct': current_risk_pct,
            'max_allowed': max_gap_risk_pct,
            'target_reduction': 1 - reduction_factor,
            'recommendations': recommendations
        }
    
    def should_close_friday(self, position: Dict, account_balance: float,
                           max_position_gap_risk_pct: float = 2.0) -> Dict:
        """Determine if a position should be closed Friday."""
        typical_gap = self.TYPICAL_GAP_SIZES.get(position['pair'], 20)
        worst_case = typical_gap * 3
        
        pip_value = 10
        lots = position['units'] / 100000
        potential_loss = worst_case * pip_value * lots
        
        risk_pct = (potential_loss / account_balance) * 100
        
        # Decision factors
        close_reasons = []
        if risk_pct > max_position_gap_risk_pct:
            close_reasons.append('excessive_gap_risk')
        if position.get('current_pnl_pips', 0) < -20:
            close_reasons.append('losing_position')
        if self.upcoming_events:
            close_reasons.append('upcoming_events')
        
        return {
            'should_close': len(close_reasons) >= 2,
            'risk_pct': risk_pct,
            'reasons': close_reasons,
            'recommendation': 'close' if len(close_reasons) >= 2 else ('reduce' if close_reasons else 'hold')
        }

# Demo
gap_manager = GapRiskManager()

# Add positions
gap_manager.add_position('EURUSD', 'long', 200000, 25)  # Winning
gap_manager.add_position('GBPUSD', 'long', 100000, -15)  # Losing
gap_manager.add_position('USDJPY', 'short', 150000, 10)  # Winning

# Set upcoming events
gap_manager.set_upcoming_events(['G7_meeting'])

print("Gap Risk Analysis:")
risk = gap_manager.calculate_gap_risk()
print(f"  Total Gap Risk: ${risk['total_gap_risk']:,.2f}")
print(f"  Event Multiplier: {risk['event_multiplier']}x")

print("\nPosition Reduction Advice (50k account):")
advice = gap_manager.get_position_reduction_advice(50000, max_gap_risk_pct=5.0)
print(f"  Reduce Needed: {advice['reduce_needed']}")
print(f"  Current Risk: {advice['current_risk_pct']:.1f}%")
if advice['reduce_needed']:
    print("  Recommendations:")
    for rec in advice['recommendations']:
        print(f"    {rec['pair']}: {rec['priority']} priority, reduce {rec['suggested_reduction']:.0%}")

Exercises

Exercise 1: Forex Position Calculator (Guided)

Complete the ForexPositionCalculator class for comprehensive position sizing.

class ForexPositionCalculator:
    """Complete forex position calculator."""
    
    def __init__(self, account_balance: float, account_currency: str = 'USD'):
        self.account_balance = account_balance
        self.account_currency = account_currency
        self.exchange_rates: Dict[str, float] = {}
    
    def set_rate(self, pair: str, rate: float) -> None:
        """Set exchange rate."""
        self.exchange_rates[pair] = rate
    
    def get_pip_value(self, pair: str, lot_size: float = 1.0) -> float:
        """Calculate pip value in account currency."""
        pip_size = 0.01 if 'JPY' in pair else ______
        units = lot_size * 100000
        pip_value = pip_size * units
        
        quote = pair[-3:]
        if quote == self.account_currency:
            return pip_value
        
        # Convert to account currency
        conversion_pair = f"{quote}{self.account_currency}"
        if conversion_pair in self.exchange_rates:
            return pip_value * self.exchange_rates[conversion_pair]
        
        reverse = f"{self.account_currency}{quote}"
        if reverse in self.exchange_rates:
            return pip_value / self.exchange_rates[reverse]
        
        return pip_value
    
    def calculate_position(self, pair: str, risk_pct: float, 
                          stop_pips: float) -> Dict:
        """Calculate position size."""
        risk_amount = self.account_balance * (risk_pct / ______)
        pip_value = self.get_pip_value(pair, 1.0)
        
        lots = risk_amount / (stop_pips * pip_value)
        
        return {
            'pair': pair,
            'risk_amount': risk_amount,
            'lots': round(lots, 2),
            'units': int(lots * 100000),
            'pip_value': pip_value
        }
    
    def validate_position(self, lots: float, pair: str, 
                         max_risk_pct: float = 2.0,
                         stop_pips: float = 30) -> Dict:
        """Validate if position size is acceptable."""
        pip_value = self.get_pip_value(pair, lots)
        risk = stop_pips * pip_value
        risk_pct = (risk / self.account_balance) * 100
        
        return {
            'valid': risk_pct <= ______,
            'risk_pct': risk_pct,
            'max_allowed': max_risk_pct
        }

# Test
calc = ForexPositionCalculator(10000, 'USD')
calc.set_rate('EURUSD', 1.0850)
calc.set_rate('USDJPY', 150.50)

print(f"Position for 1% risk, 25 pip stop: {calc.calculate_position('EURUSD', 1.0, 25)}")
print(f"Validation: {calc.validate_position(0.5, 'EURUSD', 2.0, 30)}")
Solution 1
class ForexPositionCalculator:
    def __init__(self, account_balance: float, account_currency: str = 'USD'):
        self.account_balance = account_balance
        self.account_currency = account_currency
        self.exchange_rates: Dict[str, float] = {}

    def set_rate(self, pair: str, rate: float) -> None:
        self.exchange_rates[pair] = rate

    def get_pip_value(self, pair: str, lot_size: float = 1.0) -> float:
        pip_size = 0.01 if 'JPY' in pair else 0.0001
        units = lot_size * 100000
        pip_value = pip_size * units
        # ... conversion logic
        return pip_value

    def calculate_position(self, pair: str, risk_pct: float, stop_pips: float) -> Dict:
        risk_amount = self.account_balance * (risk_pct / 100)
        pip_value = self.get_pip_value(pair, 1.0)
        lots = risk_amount / (stop_pips * pip_value)
        return {'pair': pair, 'risk_amount': risk_amount, 'lots': round(lots, 2), ...}

    def validate_position(self, lots, pair, max_risk_pct=2.0, stop_pips=30):
        # ...
        return {'valid': risk_pct <= max_risk_pct, ...}

Exercise 2: Futures Margin Monitor (Guided)

Complete the MarginMonitor class for tracking futures margin.

class MarginMonitor:
    """Monitor futures margin levels."""
    
    CONTRACTS = {
        'ES': {'initial': 13200, 'maintenance': 12000},
        'NQ': {'initial': 17600, 'maintenance': 16000},
        'CL': {'initial': 6500, 'maintenance': 5900},
    }
    
    def __init__(self, account_balance: float):
        self.account_balance = account_balance
        self.positions: Dict[str, int] = {}  # symbol -> contracts
        self.unrealized_pnl: float = 0
    
    def add_position(self, symbol: str, contracts: int) -> None:
        """Add or update position."""
        self.positions[symbol] = self.positions.get(symbol, 0) + contracts
    
    def set_unrealized_pnl(self, pnl: float) -> None:
        """Set current unrealized P&L."""
        self.unrealized_pnl = pnl
    
    def get_margin_required(self) -> float:
        """Calculate total margin required."""
        total = 0
        for symbol, contracts in self.positions.______():
            if symbol in self.CONTRACTS:
                total += abs(contracts) * self.CONTRACTS[symbol]['initial']
        return total
    
    def get_maintenance_margin(self) -> float:
        """Calculate maintenance margin."""
        total = 0
        for symbol, contracts in self.positions.items():
            if symbol in self.CONTRACTS:
                total += abs(contracts) * self.CONTRACTS[symbol]['______']
        return total
    
    def get_margin_status(self) -> Dict:
        """Get current margin status."""
        equity = self.account_balance + self.unrealized_pnl
        margin_used = self.get_margin_required()
        maintenance = self.get_maintenance_margin()
        
        margin_level = (equity / margin_used * 100) if margin_used > 0 else 100
        
        if margin_level > 150:
            status = 'healthy'
        elif margin_level > ______:
            status = 'warning'
        else:
            status = 'margin_call'
        
        return {
            'equity': equity,
            'margin_used': margin_used,
            'margin_level': margin_level,
            'status': status,
            'distance_to_call': equity - maintenance
        }

# Test
monitor = MarginMonitor(50000)
monitor.add_position('ES', 2)
monitor.add_position('CL', 1)
monitor.set_unrealized_pnl(-2000)

print(f"Margin Status: {monitor.get_margin_status()}")
Solution 2
class MarginMonitor:
    # ... CONTRACTS dict ...

    def get_margin_required(self) -> float:
        total = 0
        for symbol, contracts in self.positions.items():
            if symbol in self.CONTRACTS:
                total += abs(contracts) * self.CONTRACTS[symbol]['initial']
        return total

    def get_maintenance_margin(self) -> float:
        total = 0
        for symbol, contracts in self.positions.items():
            if symbol in self.CONTRACTS:
                total += abs(contracts) * self.CONTRACTS[symbol]['maintenance']
        return total

    def get_margin_status(self) -> Dict:
        # ...
        if margin_level > 150:
            status = 'healthy'
        elif margin_level > 100:
            status = 'warning'
        else:
            status = 'margin_call'
        # ...

Exercise 3: Weekend Risk Advisor (Guided)

Complete the WeekendRiskAdvisor class for Friday position management.

class WeekendRiskAdvisor:
    """Advise on positions before weekend."""
    
    AVG_GAPS = {'EURUSD': 15, 'GBPUSD': 20, 'USDJPY': 25}
    
    def __init__(self, account_balance: float, max_weekend_risk_pct: float = 3.0):
        self.account_balance = account_balance
        self.max_risk = max_weekend_risk_pct
        self.positions: List[Dict] = []
    
    def add_position(self, pair: str, lots: float, pnl_pips: float) -> None:
        """Add position."""
        self.positions.append({
            'pair': pair,
            'lots': lots,
            'pnl_pips': pnl_pips
        })
    
    def calculate_weekend_risk(self) -> float:
        """Calculate total weekend gap risk."""
        total_risk = 0
        for pos in self.positions:
            gap = self.AVG_GAPS.get(pos['pair'], 20) * 3  # 3x for worst case
            risk = gap * pos['lots'] * ______  # pip value
            total_risk += risk
        return total_risk
    
    def get_advice(self) -> Dict:
        """Get weekend risk advice."""
        risk = self.calculate_weekend_risk()
        risk_pct = (risk / self.account_balance) * 100
        
        if risk_pct <= self.max_risk:
            return {'action': 'hold', 'risk_pct': risk_pct}
        
        # Recommend closing losing positions first
        to_close = []
        for pos in sorted(self.positions, key=lambda x: x['______']):
            if pos['pnl_pips'] < 0:
                to_close.append(pos['pair'])
        
        return {
            'action': 'reduce',
            'risk_pct': risk_pct,
            'close_first': to_close
        }

# Test
advisor = WeekendRiskAdvisor(20000, 3.0)
advisor.add_position('EURUSD', 0.5, 30)
advisor.add_position('GBPUSD', 0.3, -15)

print(f"Weekend Advice: {advisor.get_advice()}")
Solution 3
class WeekendRiskAdvisor:
    # ...

    def calculate_weekend_risk(self) -> float:
        total_risk = 0
        for pos in self.positions:
            gap = self.AVG_GAPS.get(pos['pair'], 20) * 3
            risk = gap * pos['lots'] * 10  # pip value = $10/lot
            total_risk += risk
        return total_risk

    def get_advice(self) -> Dict:
        risk = self.calculate_weekend_risk()
        risk_pct = (risk / self.account_balance) * 100

        if risk_pct <= self.max_risk:
            return {'action': 'hold', 'risk_pct': risk_pct}

        # Sort by pnl_pips to close losers first
        to_close = []
        for pos in sorted(self.positions, key=lambda x: x['pnl_pips']):
            if pos['pnl_pips'] < 0:
                to_close.append(pos['pair'])

        return {'action': 'reduce', 'risk_pct': risk_pct, 'close_first': to_close}

Exercise 4: Complete Risk Dashboard (Open-ended)

Build a comprehensive risk dashboard that: - Tracks all positions (forex and futures) - Calculates total account risk - Monitors margin levels - Provides alerts and recommendations

# Exercise 4: Complete Risk Dashboard (Open-ended)
#
# Requirements:
# 1. Create class RiskDashboard
# 2. Track forex and futures positions
# 3. Calculate total risk as % of account
# 4. Monitor margin utilization
# 5. Generate alerts when risk exceeds limits
# 6. Provide position reduction recommendations
#
# Your implementation:
Solution 4
class RiskDashboard:
    def __init__(self, account_balance: float):
        self.balance = account_balance
        self.forex_positions: List[Dict] = []
        self.futures_positions: List[Dict] = []
        self.max_risk_pct = 5.0
        self.max_margin_pct = 50.0

    def add_forex(self, pair, lots, stop_pips, direction):
        risk = stop_pips * lots * 10
        self.forex_positions.append({
            'pair': pair, 'lots': lots, 'risk': risk, 'direction': direction
        })

    def add_futures(self, symbol, contracts, stop_ticks, tick_value, margin):
        risk = stop_ticks * tick_value * contracts
        self.futures_positions.append({
            'symbol': symbol, 'contracts': contracts, 'risk': risk, 'margin': margin * contracts
        })

    def total_risk(self):
        fx_risk = sum(p['risk'] for p in self.forex_positions)
        fut_risk = sum(p['risk'] for p in self.futures_positions)
        return fx_risk + fut_risk

    def total_margin(self):
        return sum(p['margin'] for p in self.futures_positions)

    def get_alerts(self):
        alerts = []
        risk_pct = self.total_risk() / self.balance * 100
        margin_pct = self.total_margin() / self.balance * 100

        if risk_pct > self.max_risk_pct:
            alerts.append(f'RISK: {risk_pct:.1f}% exceeds {self.max_risk_pct}%')
        if margin_pct > self.max_margin_pct:
            alerts.append(f'MARGIN: {margin_pct:.1f}% exceeds {self.max_margin_pct}%')
        return alerts

    def get_status(self):
        return {
            'total_risk': self.total_risk(),
            'risk_pct': self.total_risk() / self.balance * 100,
            'margin_used': self.total_margin(),
            'margin_pct': self.total_margin() / self.balance * 100,
            'alerts': self.get_alerts()
        }

Exercise 5: Correlation-Adjusted Position Sizer (Open-ended)

Build a position sizer that accounts for correlations between positions.

# Exercise 5: Correlation-Adjusted Position Sizer (Open-ended)
#
# Requirements:
# 1. Create class CorrelationAdjustedSizer
# 2. Store known correlations between pairs
# 3. When adding new position, check correlation with existing
# 4. Reduce size if highly correlated (adds risk)
# 5. Allow larger size if negative correlation (hedges)
#
# Your implementation:
Solution 5
class CorrelationAdjustedSizer:
    CORRELATIONS = {
        ('EURUSD', 'GBPUSD'): 0.85,
        ('EURUSD', 'USDCHF'): -0.90,
        ('AUDUSD', 'NZDUSD'): 0.90,
    }

    def __init__(self, base_risk_pct: float = 1.0):
        self.base_risk = base_risk_pct
        self.positions: Dict[str, str] = {}  # pair -> direction

    def get_correlation(self, pair1: str, pair2: str) -> float:
        key = (pair1, pair2) if (pair1, pair2) in self.CORRELATIONS else (pair2, pair1)
        return self.CORRELATIONS.get(key, 0.0)

    def calculate_adjusted_size(self, new_pair: str, new_direction: str) -> float:
        max_corr = 0
        adds_risk = False

        for pair, direction in self.positions.items():
            corr = self.get_correlation(new_pair, pair)
            effective_corr = corr if direction == new_direction else -corr

            if abs(corr) > abs(max_corr):
                max_corr = corr
                adds_risk = effective_corr > 0

        # Adjust risk based on correlation
        if adds_risk and abs(max_corr) > 0.7:
            return self.base_risk * (1 - abs(max_corr) * 0.5)  # Reduce up to 50%
        elif not adds_risk and abs(max_corr) > 0.7:
            return self.base_risk * (1 + abs(max_corr) * 0.25)  # Increase up to 25%

        return self.base_risk

    def add_position(self, pair: str, direction: str) -> None:
        self.positions[pair] = direction

Exercise 6: Dynamic Stop Manager (Open-ended)

Build a stop loss manager that dynamically adjusts stops based on market conditions.

# Exercise 6: Dynamic Stop Manager (Open-ended)
#
# Requirements:
# 1. Create class DynamicStopManager
# 2. Track positions with entry, current stop, direction
# 3. Calculate ATR-based trailing stop
# 4. Implement break-even stop after X pips profit
# 5. Tighten stop as profit increases
# 6. Never move stop against the trade
#
# Your implementation:
Solution 6
class DynamicStopManager:
    def __init__(self, breakeven_pips: float = 20, atr_multiplier: float = 2.0):
        self.breakeven_pips = breakeven_pips
        self.atr_mult = atr_multiplier
        self.positions: Dict[str, Dict] = {}

    def add_position(self, id: str, entry: float, stop: float, direction: str):
        self.positions[id] = {
            'entry': entry, 'stop': stop, 'direction': direction, 'highest': entry
        }

    def update_stop(self, id: str, current_price: float, atr: float) -> Dict:
        pos = self.positions.get(id)
        if not pos:
            return {'error': 'Position not found'}

        pip = 0.0001
        profit_pips = (current_price - pos['entry']) / pip if pos['direction'] == 'long' else (pos['entry'] - current_price) / pip

        # Update highest/lowest
        if pos['direction'] == 'long':
            pos['highest'] = max(pos['highest'], current_price)
        else:
            pos['highest'] = min(pos['highest'], current_price)

        new_stop = pos['stop']

        # Break-even
        if profit_pips >= self.breakeven_pips:
            if pos['direction'] == 'long':
                new_stop = max(new_stop, pos['entry'] + 1 * pip)
            else:
                new_stop = min(new_stop, pos['entry'] - 1 * pip)

        # ATR trailing
        if profit_pips >= self.breakeven_pips * 2:
            trail_stop = pos['highest'] - (atr * self.atr_mult) if pos['direction'] == 'long' else pos['highest'] + (atr * self.atr_mult)
            if pos['direction'] == 'long':
                new_stop = max(new_stop, trail_stop)
            else:
                new_stop = min(new_stop, trail_stop)

        pos['stop'] = new_stop
        return {'new_stop': new_stop, 'profit_pips': profit_pips}

Module Project: Risk Management System

Build a production-ready risk management system for forex and futures trading.

class RiskManagementSystem:
    """
    Production-ready risk management system.
    
    Features: Position sizing, Margin monitoring, Correlation risk,
    Gap risk, Dynamic stops, Real-time alerts.
    """
    
    def __init__(self, account_balance: float, account_currency: str = 'USD'):
        self.account_balance = account_balance
        self.account_currency = account_currency
        
        # Sub-systems
        self.forex_calc = ForexRiskCalculator(account_currency)
        self.futures_calc = FuturesRiskCalculator()
        self.exposure_calc = CurrencyExposureCalculator(account_currency)
        self.corr_manager = CorrelationRiskManager()
        self.gap_manager = GapRiskManager()
        
        # Risk limits
        self.max_single_trade_risk = 2.0  # %
        self.max_total_risk = 6.0  # %
        self.max_correlation_risk = 4.0  # %
        self.max_margin_usage = 50.0  # %
        
        # Active positions
        self.forex_positions: List[Dict] = []
        self.futures_positions: List[Dict] = []
    
    def set_exchange_rate(self, pair: str, rate: float) -> None:
        """Set exchange rate for calculations."""
        self.forex_calc.set_exchange_rate(pair, rate)
    
    def calculate_forex_position(self, pair: str, risk_pct: float,
                                stop_pips: float) -> Dict:
        """Calculate forex position size."""
        if risk_pct > self.max_single_trade_risk:
            return {'error': f'Risk {risk_pct}% exceeds max {self.max_single_trade_risk}%'}
        
        return self.forex_calc.calculate_position_size(
            self.account_balance, risk_pct, stop_pips, pair
        )
    
    def calculate_futures_position(self, symbol: str, risk_pct: float,
                                  stop_ticks: float) -> Dict:
        """Calculate futures position size."""
        if risk_pct > self.max_single_trade_risk:
            return {'error': f'Risk {risk_pct}% exceeds max {self.max_single_trade_risk}%'}
        
        return self.futures_calc.calculate_position_size(
            symbol, self.account_balance, risk_pct, stop_ticks
        )
    
    def add_forex_position(self, pair: str, direction: str,
                          units: float, entry: float, stop_pips: float) -> None:
        """Add active forex position."""
        pip_value = self.forex_calc.calculate_pip_value(pair, units / 100000)
        risk = stop_pips * pip_value
        
        self.forex_positions.append({
            'pair': pair,
            'direction': direction,
            'units': units,
            'entry': entry,
            'stop_pips': stop_pips,
            'risk': risk
        })
        
        # Update sub-systems
        self.exposure_calc.add_position(pair, direction, units, entry)
        self.corr_manager.add_position(pair, direction, risk)
        self.gap_manager.add_position(pair, direction, units, 0)
    
    def add_futures_position(self, symbol: str, direction: str,
                            contracts: int, stop_ticks: float) -> None:
        """Add active futures position."""
        risk_info = self.futures_calc.calculate_tick_risk(symbol, stop_ticks, contracts)
        contract = self.futures_calc.get_contract(symbol)
        
        self.futures_positions.append({
            'symbol': symbol,
            'direction': direction,
            'contracts': contracts,
            'stop_ticks': stop_ticks,
            'risk': risk_info['total_risk'],
            'margin': contract.initial_margin * contracts if contract else 0
        })
    
    def get_total_risk(self) -> Dict:
        """Calculate total portfolio risk."""
        forex_risk = sum(p['risk'] for p in self.forex_positions)
        futures_risk = sum(p['risk'] for p in self.futures_positions)
        
        # Get correlated risk
        corr_risk = self.corr_manager.calculate_correlated_risk()
        
        simple_total = forex_risk + futures_risk
        correlated_total = corr_risk.get('correlated_risk', simple_total)
        
        return {
            'forex_risk': forex_risk,
            'futures_risk': futures_risk,
            'simple_total': simple_total,
            'correlated_total': correlated_total,
            'risk_pct': (correlated_total / self.account_balance) * 100,
            'diversification_benefit': simple_total - correlated_total
        }
    
    def get_margin_status(self) -> Dict:
        """Get futures margin status."""
        total_margin = sum(p['margin'] for p in self.futures_positions)
        margin_pct = (total_margin / self.account_balance) * 100
        
        return {
            'margin_used': total_margin,
            'margin_pct': margin_pct,
            'available': self.account_balance - total_margin,
            'within_limits': margin_pct <= self.max_margin_usage
        }
    
    def get_alerts(self) -> List[str]:
        """Get risk alerts."""
        alerts = []
        
        # Total risk check
        risk = self.get_total_risk()
        if risk['risk_pct'] > self.max_total_risk:
            alerts.append(f"TOTAL RISK: {risk['risk_pct']:.1f}% exceeds {self.max_total_risk}%")
        
        # Margin check
        margin = self.get_margin_status()
        if not margin['within_limits']:
            alerts.append(f"MARGIN: {margin['margin_pct']:.1f}% exceeds {self.max_margin_usage}%")
        
        # Concentration check
        conc = self.exposure_calc.check_concentration(30)
        if conc['concentrated']:
            alerts.extend([f"CONCENTRATION: {w}" for w in conc['warnings']])
        
        return alerts
    
    def can_add_trade(self, risk_amount: float) -> Dict:
        """Check if a new trade can be added within limits."""
        current_risk = self.get_total_risk()['correlated_total']
        new_total = current_risk + risk_amount
        new_pct = (new_total / self.account_balance) * 100
        
        return {
            'can_add': new_pct <= self.max_total_risk,
            'current_risk_pct': (current_risk / self.account_balance) * 100,
            'new_risk_pct': new_pct,
            'max_allowed': self.max_total_risk,
            'headroom': self.max_total_risk - new_pct
        }
    
    def print_dashboard(self) -> None:
        """Print risk dashboard."""
        print("\n" + "=" * 60)
        print("  RISK MANAGEMENT DASHBOARD")
        print(f"  Account: ${self.account_balance:,.2f} {self.account_currency}")
        print("=" * 60)
        
        # Positions
        print("\n" + "-" * 40)
        print("POSITIONS")
        print("-" * 40)
        print(f"Forex: {len(self.forex_positions)} positions")
        for p in self.forex_positions:
            print(f"  {p['pair']} {p['direction']}: {p['units']:,} units, Risk: ${p['risk']:.2f}")
        print(f"Futures: {len(self.futures_positions)} positions")
        for p in self.futures_positions:
            print(f"  {p['symbol']} {p['direction']}: {p['contracts']} contracts, Risk: ${p['risk']:.2f}")
        
        # Risk Summary
        print("\n" + "-" * 40)
        print("RISK SUMMARY")
        print("-" * 40)
        risk = self.get_total_risk()
        print(f"Total Risk: ${risk['correlated_total']:.2f} ({risk['risk_pct']:.1f}%)")
        print(f"Diversification Benefit: ${risk['diversification_benefit']:.2f}")
        
        # Margin
        print("\n" + "-" * 40)
        print("MARGIN STATUS")
        print("-" * 40)
        margin = self.get_margin_status()
        print(f"Margin Used: ${margin['margin_used']:,.2f} ({margin['margin_pct']:.1f}%)")
        print(f"Available: ${margin['available']:,.2f}")
        
        # Alerts
        alerts = self.get_alerts()
        if alerts:
            print("\n" + "-" * 40)
            print("ALERTS")
            print("-" * 40)
            for alert in alerts:
                print(f"  ! {alert}")
        else:
            print("\n  All risk metrics within limits.")
        
        print("\n" + "=" * 60)
# Demo the risk management system
rms = RiskManagementSystem(account_balance=50000, account_currency='USD')

# Set exchange rates
rms.set_exchange_rate('EURUSD', 1.0850)
rms.set_exchange_rate('USDJPY', 150.50)

# Add forex positions
rms.add_forex_position('EURUSD', 'long', 100000, 1.0850, 30)
rms.add_forex_position('GBPUSD', 'long', 50000, 1.2650, 40)
rms.add_forex_position('USDJPY', 'short', 100000, 150.50, 50)

# Add futures position
rms.add_futures_position('ES', 'long', 2, 20)

# Print dashboard
rms.print_dashboard()

# Check if we can add another trade
print("\nCan add $300 risk trade?")
print(rms.can_add_trade(300))

Key Takeaways

  • Position Sizing: Always size positions based on risk %, not profit potential; typically 1-2% per trade
  • Pip Value: Varies by pair and quote currency; always calculate in account currency
  • Futures Risk: Based on tick values and contract specifications; watch margin requirements
  • ATR Stops: Dynamic stops that adapt to market volatility; typically 1.5-3x ATR
  • Correlation Risk: Same-direction correlated positions multiply risk; opposite directions hedge
  • Currency Exposure: Monitor net exposure by currency to avoid concentration
  • Weekend Gaps: Reduce position size before weekends; close losing trades first
  • Margin Management: Keep margin usage under 50% to handle adverse moves

Next: Module 10 - Backtesting where we'll build robust backtesting systems for forex and futures.

Module 10: Backtesting

Part 3: Risk & Execution

Duration Exercises
~2.5 hours 6

Learning Objectives

  • Account for realistic forex/futures costs (spreads, swaps, commissions)
  • Build a leveraged backtester with margin tracking
  • Understand tick data backtesting and variable spreads
  • Implement walk-forward optimization for robustness

Prerequisites

  • Modules 1-9 (Forex/Futures fundamentals, strategies, risk management)
  • Basic backtesting concepts
  • Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional, Callable
from enum import Enum
import warnings
warnings.filterwarnings('ignore')

10.1 Backtesting Considerations

Forex and futures backtesting requires accounting for spreads, swap/rollover costs, and leverage effects.

Trading Costs Overview

Cost Type Forex Futures Impact
Spread 0.5-3 pips 0.25-1 tick Per trade
Commission $0-7/lot $2-5/contract Per trade
Swap/Rollover Variable N/A Daily (forex)
Slippage 0.5-2 pips 1-2 ticks Market orders
@dataclass
class TradingCosts:
    """Trading costs configuration."""
    spread_pips: float = 1.0  # Forex spread
    commission_per_lot: float = 0.0  # Commission per lot
    swap_long: float = 0.0  # Daily swap for long (pips)
    swap_short: float = 0.0  # Daily swap for short (pips)
    slippage_pips: float = 0.5  # Average slippage
    
    def total_entry_cost_pips(self, is_long: bool) -> float:
        """Total cost to enter a trade in pips."""
        return self.spread_pips / 2 + self.slippage_pips
    
    def total_exit_cost_pips(self, is_long: bool) -> float:
        """Total cost to exit a trade in pips."""
        return self.spread_pips / 2 + self.slippage_pips
    
    def holding_cost_pips(self, is_long: bool, days: int) -> float:
        """Total swap cost for holding period."""
        daily_swap = self.swap_long if is_long else self.swap_short
        return daily_swap * days
    
    def commission_pips(self, lots: float, pip_value: float = 10) -> float:
        """Commission converted to pips."""
        total_commission = self.commission_per_lot * lots * 2  # Entry + exit
        return total_commission / pip_value


class CostCalculator:
    """Calculate realistic trading costs."""
    
    # Typical costs by pair
    FOREX_COSTS = {
        'EURUSD': TradingCosts(spread_pips=0.8, swap_long=-0.5, swap_short=0.3),
        'GBPUSD': TradingCosts(spread_pips=1.2, swap_long=-0.6, swap_short=0.4),
        'USDJPY': TradingCosts(spread_pips=1.0, swap_long=0.8, swap_short=-1.2),
        'AUDUSD': TradingCosts(spread_pips=1.0, swap_long=0.3, swap_short=-0.5),
        'USDCAD': TradingCosts(spread_pips=1.5, swap_long=-0.4, swap_short=0.2),
    }
    
    def __init__(self):
        self.custom_costs: Dict[str, TradingCosts] = {}
    
    def set_costs(self, symbol: str, costs: TradingCosts) -> None:
        """Set custom costs for a symbol."""
        self.custom_costs[symbol] = costs
    
    def get_costs(self, symbol: str) -> TradingCosts:
        """Get costs for a symbol."""
        if symbol in self.custom_costs:
            return self.custom_costs[symbol]
        return self.FOREX_COSTS.get(symbol, TradingCosts())
    
    def calculate_trade_cost(self, symbol: str, lots: float,
                            is_long: bool, holding_days: int = 0) -> Dict:
        """Calculate total cost for a trade."""
        costs = self.get_costs(symbol)
        pip_value = 10 * lots  # Assuming standard pip value
        
        entry_pips = costs.total_entry_cost_pips(is_long)
        exit_pips = costs.total_exit_cost_pips(is_long)
        holding_pips = costs.holding_cost_pips(is_long, holding_days)
        commission_pips = costs.commission_pips(lots)
        
        total_pips = entry_pips + exit_pips + holding_pips + commission_pips
        total_dollars = total_pips * pip_value
        
        return {
            'symbol': symbol,
            'lots': lots,
            'direction': 'long' if is_long else 'short',
            'holding_days': holding_days,
            'entry_cost_pips': entry_pips,
            'exit_cost_pips': exit_pips,
            'swap_cost_pips': holding_pips,
            'commission_pips': commission_pips,
            'total_cost_pips': total_pips,
            'total_cost_dollars': total_dollars
        }
    
    def break_even_pips(self, symbol: str, lots: float,
                        is_long: bool, holding_days: int = 0) -> float:
        """Calculate pips needed to break even."""
        cost = self.calculate_trade_cost(symbol, lots, is_long, holding_days)
        return cost['total_cost_pips']

# Demo
cost_calc = CostCalculator()

print("Trade Cost Calculation:")
cost = cost_calc.calculate_trade_cost('EURUSD', lots=1.0, is_long=True, holding_days=5)
for k, v in cost.items():
    if isinstance(v, float):
        print(f"  {k}: {v:.2f}")
    else:
        print(f"  {k}: {v}")

print(f"\nBreak-even pips: {cost_calc.break_even_pips('EURUSD', 1.0, True, 5):.2f}")

10.2 Building Backtester

A forex-specific backtester must handle leverage, margin, and realistic execution.

@dataclass
class Trade:
    """Represents a single trade."""
    id: int
    symbol: str
    direction: str  # 'long' or 'short'
    entry_time: datetime
    entry_price: float
    units: float
    stop_loss: float
    take_profit: float = None
    exit_time: datetime = None
    exit_price: float = None
    exit_reason: str = None
    pnl: float = 0.0
    pnl_pips: float = 0.0


class ForexBacktester:
    """Forex-specific backtester with leverage and margin."""
    
    def __init__(self, initial_capital: float, leverage: float = 50,
                 risk_per_trade: float = 1.0):
        self.initial_capital = initial_capital
        self.leverage = leverage
        self.risk_per_trade = risk_per_trade
        
        self.cost_calculator = CostCalculator()
        
        # State
        self.equity = initial_capital
        self.balance = initial_capital
        self.margin_used = 0.0
        self.open_trades: List[Trade] = []
        self.closed_trades: List[Trade] = []
        self.equity_curve: List[Dict] = []
        self.trade_counter = 0
    
    def reset(self) -> None:
        """Reset backtester state."""
        self.equity = self.initial_capital
        self.balance = self.initial_capital
        self.margin_used = 0.0
        self.open_trades = []
        self.closed_trades = []
        self.equity_curve = []
        self.trade_counter = 0
    
    def calculate_position_size(self, symbol: str, stop_pips: float) -> float:
        """Calculate position size based on risk."""
        risk_amount = self.equity * (self.risk_per_trade / 100)
        pip_value_per_unit = 0.0001 if 'JPY' not in symbol else 0.01
        
        # Units = Risk Amount / (Stop Pips * Pip Value)
        units = risk_amount / (stop_pips * pip_value_per_unit)
        
        # Check margin constraint
        margin_required = units / self.leverage
        available_margin = self.equity - self.margin_used
        
        if margin_required > available_margin:
            units = available_margin * self.leverage
        
        return int(units)
    
    def open_trade(self, symbol: str, direction: str, 
                   entry_time: datetime, entry_price: float,
                   stop_pips: float, tp_pips: float = None) -> Optional[Trade]:
        """Open a new trade."""
        units = self.calculate_position_size(symbol, stop_pips)
        if units <= 0:
            return None
        
        pip_size = 0.0001 if 'JPY' not in symbol else 0.01
        
        # Calculate stops
        if direction == 'long':
            stop_loss = entry_price - (stop_pips * pip_size)
            take_profit = entry_price + (tp_pips * pip_size) if tp_pips else None
        else:
            stop_loss = entry_price + (stop_pips * pip_size)
            take_profit = entry_price - (tp_pips * pip_size) if tp_pips else None
        
        # Apply entry costs (slippage)
        costs = self.cost_calculator.get_costs(symbol)
        slippage = costs.slippage_pips * pip_size
        if direction == 'long':
            entry_price += slippage
        else:
            entry_price -= slippage
        
        self.trade_counter += 1
        trade = Trade(
            id=self.trade_counter,
            symbol=symbol,
            direction=direction,
            entry_time=entry_time,
            entry_price=entry_price,
            units=units,
            stop_loss=stop_loss,
            take_profit=take_profit
        )
        
        # Update margin
        self.margin_used += units / self.leverage
        self.open_trades.append(trade)
        
        return trade
    
    def close_trade(self, trade: Trade, exit_time: datetime,
                   exit_price: float, reason: str) -> None:
        """Close an open trade."""
        pip_size = 0.0001 if 'JPY' not in trade.symbol else 0.01
        
        # Apply exit costs
        costs = self.cost_calculator.get_costs(trade.symbol)
        slippage = costs.slippage_pips * pip_size
        if trade.direction == 'long':
            exit_price -= slippage
        else:
            exit_price += slippage
        
        # Calculate P&L
        if trade.direction == 'long':
            pnl_pips = (exit_price - trade.entry_price) / pip_size
        else:
            pnl_pips = (trade.entry_price - exit_price) / pip_size
        
        # Subtract spread cost
        pnl_pips -= costs.spread_pips
        
        # Calculate holding days and swap
        holding_days = (exit_time - trade.entry_time).days
        swap_pips = costs.holding_cost_pips(trade.direction == 'long', holding_days)
        pnl_pips += swap_pips  # Swap can be positive or negative
        
        # Convert to dollars
        pnl = pnl_pips * pip_size * trade.units
        
        # Update trade
        trade.exit_time = exit_time
        trade.exit_price = exit_price
        trade.exit_reason = reason
        trade.pnl = pnl
        trade.pnl_pips = pnl_pips
        
        # Update account
        self.balance += pnl
        self.equity = self.balance
        self.margin_used -= trade.units / self.leverage
        
        # Move to closed
        self.open_trades.remove(trade)
        self.closed_trades.append(trade)
    
    def update(self, current_time: datetime, current_price: float) -> None:
        """Update open trades and check stops/targets."""
        for trade in self.open_trades[:]:
            if trade.direction == 'long':
                # Check stop loss
                if current_price <= trade.stop_loss:
                    self.close_trade(trade, current_time, trade.stop_loss, 'stop_loss')
                # Check take profit
                elif trade.take_profit and current_price >= trade.take_profit:
                    self.close_trade(trade, current_time, trade.take_profit, 'take_profit')
            else:  # Short
                if current_price >= trade.stop_loss:
                    self.close_trade(trade, current_time, trade.stop_loss, 'stop_loss')
                elif trade.take_profit and current_price <= trade.take_profit:
                    self.close_trade(trade, current_time, trade.take_profit, 'take_profit')
        
        # Update equity curve
        self._update_equity(current_time, current_price)
    
    def _update_equity(self, current_time: datetime, current_price: float) -> None:
        """Update equity with unrealized P&L."""
        unrealized = 0.0
        for trade in self.open_trades:
            pip_size = 0.0001 if 'JPY' not in trade.symbol else 0.01
            if trade.direction == 'long':
                unrealized += (current_price - trade.entry_price) * trade.units
            else:
                unrealized += (trade.entry_price - current_price) * trade.units
        
        self.equity = self.balance + unrealized
        self.equity_curve.append({
            'time': current_time,
            'equity': self.equity,
            'balance': self.balance
        })
    
    def run(self, data: pd.DataFrame, strategy: Callable) -> Dict:
        """Run backtest with a strategy function."""
        self.reset()
        
        for i in range(len(data)):
            row = data.iloc[i]
            current_time = row.name if isinstance(row.name, datetime) else data.index[i]
            current_price = row['close']
            
            # Update existing trades
            self.update(current_time, current_price)
            
            # Check margin call
            if self.equity < self.margin_used * 0.5:
                # Close all trades
                for trade in self.open_trades[:]:
                    self.close_trade(trade, current_time, current_price, 'margin_call')
                continue
            
            # Generate signal from strategy
            signal = strategy(data.iloc[:i+1])
            
            if signal and len(self.open_trades) == 0:
                self.open_trade(
                    symbol=signal.get('symbol', 'EURUSD'),
                    direction=signal['direction'],
                    entry_time=current_time,
                    entry_price=current_price,
                    stop_pips=signal.get('stop_pips', 30),
                    tp_pips=signal.get('tp_pips')
                )
        
        # Close any remaining trades
        for trade in self.open_trades[:]:
            self.close_trade(trade, data.index[-1], data['close'].iloc[-1], 'end_of_test')
        
        return self.get_results()
    
    def get_results(self) -> Dict:
        """Calculate backtest statistics."""
        if not self.closed_trades:
            return {'error': 'No trades'}
        
        pnls = [t.pnl for t in self.closed_trades]
        pnl_pips = [t.pnl_pips for t in self.closed_trades]
        
        wins = [p for p in pnls if p > 0]
        losses = [p for p in pnls if p <= 0]
        
        total_pnl = sum(pnls)
        win_rate = len(wins) / len(pnls) * 100 if pnls else 0
        
        avg_win = np.mean(wins) if wins else 0
        avg_loss = abs(np.mean(losses)) if losses else 0
        profit_factor = sum(wins) / abs(sum(losses)) if losses and sum(losses) != 0 else 0
        
        # Drawdown
        equity_values = [e['equity'] for e in self.equity_curve]
        peak = equity_values[0]
        max_dd = 0
        for eq in equity_values:
            if eq > peak:
                peak = eq
            dd = (peak - eq) / peak * 100
            max_dd = max(max_dd, dd)
        
        return {
            'initial_capital': self.initial_capital,
            'final_equity': self.equity,
            'total_pnl': total_pnl,
            'return_pct': (self.equity / self.initial_capital - 1) * 100,
            'total_trades': len(self.closed_trades),
            'winning_trades': len(wins),
            'losing_trades': len(losses),
            'win_rate': win_rate,
            'avg_win': avg_win,
            'avg_loss': avg_loss,
            'profit_factor': profit_factor,
            'max_drawdown_pct': max_dd,
            'avg_pips': np.mean(pnl_pips),
            'total_pips': sum(pnl_pips)
        }

# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=250, freq='D')
prices = pd.DataFrame({
    'open': 1.0800 + np.cumsum(np.random.normal(0.0001, 0.005, 250)),
    'high': 0,
    'low': 0,
    'close': 0
}, index=dates)
prices['close'] = prices['open'] + np.random.normal(0, 0.002, 250)
prices['high'] = prices[['open', 'close']].max(axis=1) + np.random.uniform(0, 0.003, 250)
prices['low'] = prices[['open', 'close']].min(axis=1) - np.random.uniform(0, 0.003, 250)

# Simple MA crossover strategy
def ma_strategy(data: pd.DataFrame) -> Optional[Dict]:
    if len(data) < 50:
        return None
    
    fast = data['close'].rolling(20).mean().iloc[-1]
    slow = data['close'].rolling(50).mean().iloc[-1]
    prev_fast = data['close'].rolling(20).mean().iloc[-2]
    prev_slow = data['close'].rolling(50).mean().iloc[-2]
    
    # Crossover
    if prev_fast <= prev_slow and fast > slow:
        return {'direction': 'long', 'stop_pips': 30, 'tp_pips': 60}
    elif prev_fast >= prev_slow and fast < slow:
        return {'direction': 'short', 'stop_pips': 30, 'tp_pips': 60}
    
    return None

# Run backtest
backtester = ForexBacktester(initial_capital=10000, leverage=50, risk_per_trade=1.0)
results = backtester.run(prices, ma_strategy)

print("Backtest Results:")
for k, v in results.items():
    if isinstance(v, float):
        print(f"  {k}: {v:.2f}")
    else:
        print(f"  {k}: {v}")

10.3 Tick Data Backtesting

Tick data provides the most accurate simulation by capturing variable spreads and real execution conditions.

@dataclass
class Tick:
    """Single tick with bid/ask."""
    timestamp: datetime
    bid: float
    ask: float
    
    @property
    def spread(self) -> float:
        return self.ask - self.bid
    
    @property
    def mid(self) -> float:
        return (self.bid + self.ask) / 2


class TickDataBacktester:
    """Backtester using tick-level data with variable spreads."""
    
    def __init__(self, initial_capital: float, leverage: float = 50):
        self.initial_capital = initial_capital
        self.leverage = leverage
        self.equity = initial_capital
        self.position = None
        self.trades: List[Dict] = []
    
    def reset(self) -> None:
        """Reset state."""
        self.equity = self.initial_capital
        self.position = None
        self.trades = []
    
    def execute_market_order(self, tick: Tick, direction: str,
                            units: float, stop_pips: float) -> Dict:
        """Execute market order at current tick."""
        pip_size = 0.0001
        
        # Use appropriate price (ask for buy, bid for sell)
        if direction == 'long':
            entry_price = tick.ask  # Buy at ask
            stop_loss = tick.bid - (stop_pips * pip_size)
        else:
            entry_price = tick.bid  # Sell at bid
            stop_loss = tick.ask + (stop_pips * pip_size)
        
        self.position = {
            'direction': direction,
            'entry_price': entry_price,
            'entry_time': tick.timestamp,
            'units': units,
            'stop_loss': stop_loss,
            'entry_spread': tick.spread
        }
        
        return self.position
    
    def close_position(self, tick: Tick, reason: str) -> Dict:
        """Close current position."""
        if not self.position:
            return None
        
        # Use appropriate price
        if self.position['direction'] == 'long':
            exit_price = tick.bid  # Sell at bid
            pnl = (exit_price - self.position['entry_price']) * self.position['units']
        else:
            exit_price = tick.ask  # Buy at ask
            pnl = (self.position['entry_price'] - exit_price) * self.position['units']
        
        trade = {
            **self.position,
            'exit_price': exit_price,
            'exit_time': tick.timestamp,
            'exit_spread': tick.spread,
            'pnl': pnl,
            'reason': reason
        }
        
        self.equity += pnl
        self.trades.append(trade)
        self.position = None
        
        return trade
    
    def check_stop(self, tick: Tick) -> bool:
        """Check if stop loss is hit."""
        if not self.position:
            return False
        
        if self.position['direction'] == 'long':
            if tick.bid <= self.position['stop_loss']:
                self.close_position(tick, 'stop_loss')
                return True
        else:
            if tick.ask >= self.position['stop_loss']:
                self.close_position(tick, 'stop_loss')
                return True
        
        return False
    
    def analyze_spread_impact(self) -> Dict:
        """Analyze impact of spreads on results."""
        if not self.trades:
            return {}
        
        spreads = [(t['entry_spread'] + t['exit_spread']) for t in self.trades]
        spread_costs = [s * t['units'] for s, t in zip(spreads, self.trades)]
        
        return {
            'avg_spread': np.mean(spreads),
            'max_spread': max(spreads),
            'min_spread': min(spreads),
            'total_spread_cost': sum(spread_costs),
            'spread_cost_per_trade': np.mean(spread_costs)
        }

# Demo with simulated tick data
def generate_tick_data(n_ticks: int, base_price: float = 1.0800) -> List[Tick]:
    """Generate simulated tick data with variable spreads."""
    ticks = []
    price = base_price
    start_time = datetime(2024, 1, 1, 0, 0, 0)
    
    for i in range(n_ticks):
        # Random price movement
        price += np.random.normal(0, 0.00005)
        
        # Variable spread (wider during volatile times)
        base_spread = 0.00008
        spread_variation = np.random.uniform(0, 0.00005)
        spread = base_spread + spread_variation
        
        tick = Tick(
            timestamp=start_time + timedelta(seconds=i),
            bid=price - spread/2,
            ask=price + spread/2
        )
        ticks.append(tick)
    
    return ticks

# Generate ticks
np.random.seed(42)
ticks = generate_tick_data(10000)

# Run simple tick-level backtest
tick_bt = TickDataBacktester(initial_capital=10000, leverage=50)

for i, tick in enumerate(ticks):
    # Check stops first
    tick_bt.check_stop(tick)
    
    # Simple strategy: trade every 1000 ticks
    if i % 1000 == 0 and tick_bt.position is None:
        direction = 'long' if np.random.random() > 0.5 else 'short'
        tick_bt.execute_market_order(tick, direction, units=10000, stop_pips=20)
    
    # Close after 500 ticks in trade
    if tick_bt.position and i % 500 == 0:
        tick_bt.close_position(tick, 'time_exit')

print("Tick-Level Backtest:")
print(f"  Final Equity: ${tick_bt.equity:,.2f}")
print(f"  Total Trades: {len(tick_bt.trades)}")
print(f"\nSpread Analysis:")
spread_analysis = tick_bt.analyze_spread_impact()
for k, v in spread_analysis.items():
    print(f"  {k}: {v:.6f}" if 'spread' in k.lower() else f"  {k}: {v:.2f}")

10.4 Walk-Forward Optimization

Walk-forward testing validates strategy robustness by optimizing on in-sample data and testing on out-of-sample data.

class WalkForwardOptimizer:
    """Walk-forward optimization for strategy validation."""
    
    def __init__(self, backtester: ForexBacktester):
        self.backtester = backtester
        self.results: List[Dict] = []
    
    def create_windows(self, data: pd.DataFrame, 
                       is_period: int, oos_period: int,
                       step: int = None) -> List[Dict]:
        """Create in-sample and out-of-sample windows."""
        if step is None:
            step = oos_period
        
        windows = []
        start = 0
        
        while start + is_period + oos_period <= len(data):
            is_start = start
            is_end = start + is_period
            oos_start = is_end
            oos_end = oos_start + oos_period
            
            windows.append({
                'is_data': data.iloc[is_start:is_end],
                'oos_data': data.iloc[oos_start:oos_end],
                'is_dates': (data.index[is_start], data.index[is_end-1]),
                'oos_dates': (data.index[oos_start], data.index[oos_end-1])
            })
            
            start += step
        
        return windows
    
    def optimize_parameters(self, data: pd.DataFrame, 
                           param_grid: Dict,
                           strategy_factory: Callable) -> Dict:
        """Find best parameters on in-sample data."""
        best_result = None
        best_params = None
        best_metric = -np.inf
        
        # Generate all parameter combinations
        import itertools
        keys = list(param_grid.keys())
        values = list(param_grid.values())
        
        for combo in itertools.product(*values):
            params = dict(zip(keys, combo))
            
            # Create strategy with these parameters
            strategy = strategy_factory(params)
            
            # Run backtest
            self.backtester.reset()
            result = self.backtester.run(data, strategy)
            
            # Optimize for Sharpe-like metric (return / drawdown)
            if result.get('max_drawdown_pct', 100) > 0:
                metric = result.get('return_pct', 0) / result.get('max_drawdown_pct', 100)
            else:
                metric = result.get('return_pct', 0)
            
            if metric > best_metric:
                best_metric = metric
                best_params = params
                best_result = result
        
        return {
            'best_params': best_params,
            'best_result': best_result,
            'optimization_metric': best_metric
        }
    
    def run_walk_forward(self, data: pd.DataFrame,
                        is_period: int, oos_period: int,
                        param_grid: Dict,
                        strategy_factory: Callable) -> Dict:
        """Run complete walk-forward analysis."""
        windows = self.create_windows(data, is_period, oos_period)
        self.results = []
        
        for i, window in enumerate(windows):
            print(f"Window {i+1}/{len(windows)}...")
            
            # Optimize on in-sample
            opt_result = self.optimize_parameters(
                window['is_data'], param_grid, strategy_factory
            )
            
            # Test on out-of-sample with best params
            best_strategy = strategy_factory(opt_result['best_params'])
            self.backtester.reset()
            oos_result = self.backtester.run(window['oos_data'], best_strategy)
            
            self.results.append({
                'window': i + 1,
                'is_dates': window['is_dates'],
                'oos_dates': window['oos_dates'],
                'best_params': opt_result['best_params'],
                'is_result': opt_result['best_result'],
                'oos_result': oos_result
            })
        
        return self.get_summary()
    
    def get_summary(self) -> Dict:
        """Get walk-forward summary statistics."""
        if not self.results:
            return {}
        
        is_returns = [r['is_result'].get('return_pct', 0) for r in self.results]
        oos_returns = [r['oos_result'].get('return_pct', 0) for r in self.results]
        
        # Walk-forward efficiency
        wf_efficiency = np.mean(oos_returns) / np.mean(is_returns) * 100 if np.mean(is_returns) != 0 else 0
        
        return {
            'num_windows': len(self.results),
            'avg_is_return': np.mean(is_returns),
            'avg_oos_return': np.mean(oos_returns),
            'wf_efficiency': wf_efficiency,
            'oos_win_rate': sum(1 for r in oos_returns if r > 0) / len(oos_returns) * 100,
            'total_oos_return': sum(oos_returns),
            'is_oos_correlation': np.corrcoef(is_returns, oos_returns)[0, 1] if len(is_returns) > 1 else 0
        }

# Demo walk-forward
def create_ma_strategy(params: Dict) -> Callable:
    """Factory to create MA strategy with given parameters."""
    fast = params.get('fast', 10)
    slow = params.get('slow', 30)
    
    def strategy(data: pd.DataFrame) -> Optional[Dict]:
        if len(data) < slow + 5:
            return None
        
        fast_ma = data['close'].rolling(fast).mean().iloc[-1]
        slow_ma = data['close'].rolling(slow).mean().iloc[-1]
        prev_fast = data['close'].rolling(fast).mean().iloc[-2]
        prev_slow = data['close'].rolling(slow).mean().iloc[-2]
        
        if prev_fast <= prev_slow and fast_ma > slow_ma:
            return {'direction': 'long', 'stop_pips': 25, 'tp_pips': 50}
        elif prev_fast >= prev_slow and fast_ma < slow_ma:
            return {'direction': 'short', 'stop_pips': 25, 'tp_pips': 50}
        
        return None
    
    return strategy

# Run walk-forward (smaller dataset for demo)
wfo = WalkForwardOptimizer(ForexBacktester(10000, 50, 1.0))

param_grid = {
    'fast': [5, 10, 15],
    'slow': [20, 30, 40]
}

wf_results = wfo.run_walk_forward(
    data=prices,
    is_period=100,
    oos_period=50,
    param_grid=param_grid,
    strategy_factory=create_ma_strategy
)

print("\nWalk-Forward Results:")
for k, v in wf_results.items():
    print(f"  {k}: {v:.2f}" if isinstance(v, float) else f"  {k}: {v}")

Exercises

Exercise 1: Cost-Adjusted Backtester (Guided)

Complete the CostAdjustedBacktester that properly accounts for all trading costs.

class CostAdjustedBacktester:
    """Backtester with realistic cost modeling."""
    
    def __init__(self, initial_capital: float):
        self.capital = initial_capital
        self.equity = initial_capital
        self.trades: List[Dict] = []
    
    def calculate_trade_costs(self, entry: float, exit: float,
                             lots: float, days_held: int,
                             spread_pips: float = 1.0,
                             swap_per_day: float = -0.5) -> Dict:
        """Calculate all costs for a trade."""
        pip_value = 10 * lots
        
        # Spread cost
        spread_cost = spread_pips * ______
        
        # Swap cost
        swap_cost = swap_per_day * days_held * pip_value
        
        # Slippage (assume 0.5 pips each way)
        slippage_cost = ______ * pip_value * 2
        
        total_cost = spread_cost + swap_cost + slippage_cost
        
        return {
            'spread_cost': spread_cost,
            'swap_cost': swap_cost,
            'slippage_cost': slippage_cost,
            'total_cost': total_cost
        }
    
    def calculate_net_pnl(self, gross_pnl: float, costs: Dict) -> float:
        """Calculate net P&L after costs."""
        return gross_pnl - costs['______']

# Test
bt = CostAdjustedBacktester(10000)
costs = bt.calculate_trade_costs(1.0800, 1.0830, lots=1.0, days_held=3)
print(f"Trade Costs: {costs}")
print(f"Net P&L (from 30 pip gain): ${bt.calculate_net_pnl(300, costs):.2f}")
Solution 1
class CostAdjustedBacktester:
    def __init__(self, initial_capital: float):
        self.capital = initial_capital
        self.equity = initial_capital
        self.trades: List[Dict] = []

    def calculate_trade_costs(self, entry, exit, lots, days_held,
                             spread_pips=1.0, swap_per_day=-0.5):
        pip_value = 10 * lots

        spread_cost = spread_pips * pip_value
        swap_cost = swap_per_day * days_held * pip_value
        slippage_cost = 0.5 * pip_value * 2

        total_cost = spread_cost + swap_cost + slippage_cost
        return {..., 'total_cost': total_cost}

    def calculate_net_pnl(self, gross_pnl: float, costs: Dict) -> float:
        return gross_pnl - costs['total_cost']

Exercise 2: Margin Tracker (Guided)

Complete the MarginTracker class for tracking margin during backtests.

class MarginTracker:
    """Track margin levels during backtest."""
    
    def __init__(self, initial_equity: float, leverage: float = 50):
        self.equity = initial_equity
        self.leverage = leverage
        self.positions: Dict[str, float] = {}  # symbol -> units
    
    def get_margin_used(self) -> float:
        """Calculate total margin used."""
        total_units = sum(abs(u) for u in self.positions.______())
        return total_units / self.leverage
    
    def get_free_margin(self) -> float:
        """Calculate available margin."""
        return self.equity - self.______
    
    def get_margin_level(self) -> float:
        """Calculate margin level percentage."""
        used = self.get_margin_used()
        if used == 0:
            return 100.0
        return (self.equity / used) * ______
    
    def can_open_position(self, units: float, min_margin_level: float = 100) -> bool:
        """Check if new position is possible."""
        new_margin = self.get_margin_used() + (units / self.leverage)
        new_level = (self.equity / new_margin) * 100 if new_margin > 0 else 100
        return new_level >= min_margin_level

# Test
tracker = MarginTracker(10000, leverage=50)
tracker.positions['EURUSD'] = 100000
print(f"Margin Used: ${tracker.get_margin_used():,.2f}")
print(f"Free Margin: ${tracker.get_free_margin():,.2f}")
print(f"Margin Level: {tracker.get_margin_level():.1f}%")
Solution 2
class MarginTracker:
    def __init__(self, initial_equity: float, leverage: float = 50):
        self.equity = initial_equity
        self.leverage = leverage
        self.positions: Dict[str, float] = {}

    def get_margin_used(self) -> float:
        total_units = sum(abs(u) for u in self.positions.values())
        return total_units / self.leverage

    def get_free_margin(self) -> float:
        return self.equity - self.get_margin_used()

    def get_margin_level(self) -> float:
        used = self.get_margin_used()
        if used == 0:
            return 100.0
        return (self.equity / used) * 100

Exercise 3: Spread Analyzer (Guided)

Complete the SpreadAnalyzer for analyzing spread patterns in tick data.

class SpreadAnalyzer:
    """Analyze spread patterns from tick data."""
    
    def __init__(self):
        self.spreads: List[float] = []
        self.timestamps: List[datetime] = []
    
    def add_tick(self, timestamp: datetime, bid: float, ask: float) -> None:
        """Add tick data."""
        spread = ask - ______
        self.spreads.append(spread)
        self.timestamps.append(timestamp)
    
    def get_statistics(self) -> Dict:
        """Get spread statistics."""
        if not self.spreads:
            return {}
        
        return {
            'avg_spread': np.______(self.spreads),
            'median_spread': np.median(self.spreads),
            'max_spread': max(self.spreads),
            'min_spread': min(self.spreads),
            'std_spread': np.std(self.spreads)
        }
    
    def get_spread_by_hour(self) -> Dict[int, float]:
        """Get average spread by hour."""
        hourly = {}
        for ts, spread in zip(self.timestamps, self.spreads):
            hour = ts.______
            if hour not in hourly:
                hourly[hour] = []
            hourly[hour].append(spread)
        
        return {h: np.mean(s) for h, s in hourly.items()}

# Test
analyzer = SpreadAnalyzer()
for tick in ticks[:1000]:
    analyzer.add_tick(tick.timestamp, tick.bid, tick.ask)

print(f"Spread Statistics: {analyzer.get_statistics()}")
Solution 3
class SpreadAnalyzer:
    def __init__(self):
        self.spreads: List[float] = []
        self.timestamps: List[datetime] = []

    def add_tick(self, timestamp: datetime, bid: float, ask: float) -> None:
        spread = ask - bid
        self.spreads.append(spread)
        self.timestamps.append(timestamp)

    def get_statistics(self) -> Dict:
        if not self.spreads:
            return {}
        return {
            'avg_spread': np.mean(self.spreads),
            ...
        }

    def get_spread_by_hour(self) -> Dict[int, float]:
        hourly = {}
        for ts, spread in zip(self.timestamps, self.spreads):
            hour = ts.hour
            ...

Exercise 4: Complete Forex Backtester (Open-ended)

Build a full-featured forex backtester with all realistic components.

# Exercise 4: Complete Forex Backtester (Open-ended)
#
# Requirements:
# 1. Create class FullForexBacktester
# 2. Track: equity, balance, margin, open/closed trades
# 3. Include all costs: spread, swap, slippage, commission
# 4. Handle margin calls (close all if equity < margin)
# 5. Support multiple open positions
# 6. Calculate comprehensive statistics
#
# Your implementation:
Solution 4
class FullForexBacktester:
    def __init__(self, capital, leverage=50):
        self.initial = capital
        self.equity = capital
        self.balance = capital
        self.leverage = leverage
        self.margin_used = 0
        self.open_trades = []
        self.closed_trades = []
        self.costs = CostCalculator()

    def open_trade(self, symbol, direction, units, entry, stop, tp=None):
        margin = units / self.leverage
        if margin > self.equity - self.margin_used:
            return None

        # Apply entry slippage
        costs = self.costs.get_costs(symbol)
        slip = costs.slippage_pips * 0.0001
        entry = entry + slip if direction == 'long' else entry - slip

        trade = {'symbol': symbol, 'direction': direction, 'units': units,
                 'entry': entry, 'stop': stop, 'tp': tp, 'entry_time': datetime.now()}
        self.open_trades.append(trade)
        self.margin_used += margin
        return trade

    def close_trade(self, trade, exit_price, reason):
        costs = self.costs.get_costs(trade['symbol'])
        # Apply exit costs and calculate PnL
        # ...
        self.balance += pnl
        self.margin_used -= trade['units'] / self.leverage
        self.closed_trades.append(trade)
        self.open_trades.remove(trade)

    def check_margin_call(self):
        if self.equity < self.margin_used * 0.5:
            for trade in self.open_trades[:]:
                self.close_trade(trade, 'margin_call')

    def get_statistics(self):
        # Calculate win rate, profit factor, max DD, etc.
        pass

Exercise 5: Monte Carlo Simulator (Open-ended)

Build a Monte Carlo simulator to test strategy robustness.

# Exercise 5: Monte Carlo Simulator (Open-ended)
#
# Requirements:
# 1. Create class MonteCarloSimulator
# 2. Take backtest trade results as input
# 3. Randomly resample trades to create N simulations
# 4. Calculate distribution of final equity
# 5. Calculate probability of ruin (equity < X%)
# 6. Generate confidence intervals for returns
#
# Your implementation:
Solution 5
class MonteCarloSimulator:
    def __init__(self, trade_results: List[float], initial_capital: float):
        self.trades = trade_results
        self.capital = initial_capital

    def run_simulation(self, n_simulations: int = 1000) -> Dict:
        final_equities = []
        max_drawdowns = []

        for _ in range(n_simulations):
            # Randomly resample trades
            sampled = np.random.choice(self.trades, size=len(self.trades), replace=True)

            # Calculate equity curve
            equity = self.capital
            peak = equity
            max_dd = 0

            for pnl in sampled:
                equity += pnl
                if equity > peak:
                    peak = equity
                dd = (peak - equity) / peak
                max_dd = max(max_dd, dd)

            final_equities.append(equity)
            max_drawdowns.append(max_dd)

        return {
            'mean_equity': np.mean(final_equities),
            'median_equity': np.median(final_equities),
            'std_equity': np.std(final_equities),
            'percentile_5': np.percentile(final_equities, 5),
            'percentile_95': np.percentile(final_equities, 95),
            'prob_profit': sum(1 for e in final_equities if e > self.capital) / n_simulations,
            'prob_ruin': sum(1 for e in final_equities if e < self.capital * 0.5) / n_simulations,
            'avg_max_dd': np.mean(max_drawdowns)
        }

Exercise 6: Walk-Forward Report Generator (Open-ended)

Build a comprehensive walk-forward analysis report generator.

# Exercise 6: Walk-Forward Report Generator (Open-ended)
#
# Requirements:
# 1. Create class WalkForwardReporter
# 2. Take walk-forward results as input
# 3. Calculate WF efficiency (OOS return / IS return)
# 4. Track parameter stability across windows
# 5. Identify degradation trends
# 6. Generate pass/fail verdict on strategy robustness
#
# Your implementation:
Solution 6
class WalkForwardReporter:
    def __init__(self, wf_results: List[Dict]):
        self.results = wf_results

    def calculate_wf_efficiency(self) -> float:
        is_returns = [r['is_result']['return_pct'] for r in self.results]
        oos_returns = [r['oos_result']['return_pct'] for r in self.results]
        return np.mean(oos_returns) / np.mean(is_returns) * 100 if np.mean(is_returns) != 0 else 0

    def analyze_parameter_stability(self) -> Dict:
        params_by_window = [r['best_params'] for r in self.results]
        # Check how often parameters change
        stability = {}
        for param in params_by_window[0].keys():
            values = [p[param] for p in params_by_window]
            stability[param] = {
                'unique_values': len(set(values)),
                'most_common': max(set(values), key=values.count),
                'stability_score': 1 - (len(set(values)) / len(values))
            }
        return stability

    def detect_degradation(self) -> bool:
        oos_returns = [r['oos_result']['return_pct'] for r in self.results]
        # Check if returns are trending down
        if len(oos_returns) < 3:
            return False
        trend = np.polyfit(range(len(oos_returns)), oos_returns, 1)[0]
        return trend < -1  # Significant negative trend

    def get_verdict(self) -> Dict:
        wf_eff = self.calculate_wf_efficiency()
        degrading = self.detect_degradation()
        oos_wins = sum(1 for r in self.results if r['oos_result']['return_pct'] > 0)

        passed = wf_eff > 50 and not degrading and oos_wins > len(self.results) * 0.5

        return {
            'verdict': 'PASS' if passed else 'FAIL',
            'wf_efficiency': wf_eff,
            'degrading': degrading,
            'oos_win_rate': oos_wins / len(self.results) * 100,
            'reasons': [] if passed else self._get_failure_reasons(wf_eff, degrading, oos_wins)
        }

Module Project: Forex/Futures Backtester

Build a production-ready backtesting system with all features.

class ProductionBacktester:
    """
    Production-ready forex/futures backtesting system.
    
    Features: Realistic costs, Margin tracking, Walk-forward,
    Monte Carlo analysis, Comprehensive reporting.
    """
    
    def __init__(self, initial_capital: float = 10000,
                 leverage: float = 50, risk_per_trade: float = 1.0):
        self.initial_capital = initial_capital
        self.leverage = leverage
        self.risk_per_trade = risk_per_trade
        
        # Core backtester
        self.backtester = ForexBacktester(initial_capital, leverage, risk_per_trade)
        self.cost_calc = CostCalculator()
        
        # Results storage
        self.backtest_results: Dict = {}
        self.wf_results: Dict = {}
        self.mc_results: Dict = {}
    
    def run_backtest(self, data: pd.DataFrame, strategy: Callable,
                    symbol: str = 'EURUSD') -> Dict:
        """Run standard backtest."""
        # Set costs for symbol
        self.backtester.cost_calculator = self.cost_calc
        
        # Run backtest
        self.backtest_results = self.backtester.run(data, strategy)
        
        # Add cost analysis
        if self.backtester.closed_trades:
            total_spread_cost = sum(
                self.cost_calc.get_costs(symbol).spread_pips * 10 * (t.units / 100000)
                for t in self.backtester.closed_trades
            )
            self.backtest_results['total_spread_cost'] = total_spread_cost
        
        return self.backtest_results
    
    def run_walk_forward(self, data: pd.DataFrame,
                        param_grid: Dict, strategy_factory: Callable,
                        is_period: int = 100, oos_period: int = 50) -> Dict:
        """Run walk-forward optimization."""
        wfo = WalkForwardOptimizer(self.backtester)
        self.wf_results = wfo.run_walk_forward(
            data, is_period, oos_period, param_grid, strategy_factory
        )
        return self.wf_results
    
    def run_monte_carlo(self, n_simulations: int = 1000) -> Dict:
        """Run Monte Carlo analysis on backtest results."""
        if not self.backtester.closed_trades:
            return {'error': 'No trades to analyze'}
        
        pnls = [t.pnl for t in self.backtester.closed_trades]
        
        final_equities = []
        max_drawdowns = []
        
        for _ in range(n_simulations):
            sampled = np.random.choice(pnls, size=len(pnls), replace=True)
            
            equity = self.initial_capital
            peak = equity
            max_dd = 0
            
            for pnl in sampled:
                equity += pnl
                if equity > peak:
                    peak = equity
                dd = (peak - equity) / peak * 100
                max_dd = max(max_dd, dd)
            
            final_equities.append(equity)
            max_drawdowns.append(max_dd)
        
        self.mc_results = {
            'mean_equity': np.mean(final_equities),
            'median_equity': np.median(final_equities),
            'percentile_5': np.percentile(final_equities, 5),
            'percentile_95': np.percentile(final_equities, 95),
            'prob_profit': sum(1 for e in final_equities if e > self.initial_capital) / n_simulations * 100,
            'prob_ruin_50pct': sum(1 for e in final_equities if e < self.initial_capital * 0.5) / n_simulations * 100,
            'avg_max_drawdown': np.mean(max_drawdowns)
        }
        
        return self.mc_results
    
    def generate_report(self) -> None:
        """Generate comprehensive backtest report."""
        print("\n" + "=" * 70)
        print("  BACKTEST REPORT")
        print("=" * 70)
        
        # Backtest Results
        print("\n" + "-" * 50)
        print("BACKTEST RESULTS")
        print("-" * 50)
        if self.backtest_results:
            print(f"Initial Capital: ${self.initial_capital:,.2f}")
            print(f"Final Equity: ${self.backtest_results.get('final_equity', 0):,.2f}")
            print(f"Total Return: {self.backtest_results.get('return_pct', 0):.2f}%")
            print(f"Total Trades: {self.backtest_results.get('total_trades', 0)}")
            print(f"Win Rate: {self.backtest_results.get('win_rate', 0):.1f}%")
            print(f"Profit Factor: {self.backtest_results.get('profit_factor', 0):.2f}")
            print(f"Max Drawdown: {self.backtest_results.get('max_drawdown_pct', 0):.2f}%")
        
        # Walk-Forward Results
        if self.wf_results:
            print("\n" + "-" * 50)
            print("WALK-FORWARD ANALYSIS")
            print("-" * 50)
            print(f"Windows Tested: {self.wf_results.get('num_windows', 0)}")
            print(f"Avg IS Return: {self.wf_results.get('avg_is_return', 0):.2f}%")
            print(f"Avg OOS Return: {self.wf_results.get('avg_oos_return', 0):.2f}%")
            print(f"WF Efficiency: {self.wf_results.get('wf_efficiency', 0):.1f}%")
            print(f"OOS Win Rate: {self.wf_results.get('oos_win_rate', 0):.1f}%")
        
        # Monte Carlo Results
        if self.mc_results:
            print("\n" + "-" * 50)
            print("MONTE CARLO ANALYSIS")
            print("-" * 50)
            print(f"Mean Final Equity: ${self.mc_results.get('mean_equity', 0):,.2f}")
            print(f"5th Percentile: ${self.mc_results.get('percentile_5', 0):,.2f}")
            print(f"95th Percentile: ${self.mc_results.get('percentile_95', 0):,.2f}")
            print(f"Probability of Profit: {self.mc_results.get('prob_profit', 0):.1f}%")
            print(f"Probability of 50% Ruin: {self.mc_results.get('prob_ruin_50pct', 0):.1f}%")
        
        print("\n" + "=" * 70)
# Demo the production backtester
prod_bt = ProductionBacktester(initial_capital=10000, leverage=50, risk_per_trade=1.0)

# Run standard backtest
print("Running backtest...")
results = prod_bt.run_backtest(prices, ma_strategy, 'EURUSD')

# Run walk-forward
print("\nRunning walk-forward analysis...")
wf = prod_bt.run_walk_forward(
    prices, 
    param_grid={'fast': [5, 10], 'slow': [20, 30]},
    strategy_factory=create_ma_strategy,
    is_period=100,
    oos_period=50
)

# Run Monte Carlo
print("\nRunning Monte Carlo analysis...")
mc = prod_bt.run_monte_carlo(n_simulations=500)

# Generate report
prod_bt.generate_report()

Key Takeaways

  • Realistic Costs: Include spread, swap, slippage, and commission in all backtests
  • Break-Even: Calculate minimum pips needed to cover costs before expecting profit
  • Margin Tracking: Monitor margin usage to avoid margin calls; keep under 50%
  • Tick Data: Most accurate simulation; captures variable spreads and real execution
  • Walk-Forward: Validates strategy robustness; aim for >50% WF efficiency
  • Parameter Stability: Stable optimal parameters across windows indicate robustness
  • Monte Carlo: Test distribution of outcomes; understand probability of ruin
  • Over-Optimization: Beware curve-fitting; out-of-sample results matter most

Next: Module 11 - Live Trading where we'll build systems for real-world execution.

Module 11: Live Trading

Duration ~2.5 hours
Skill Level Advanced
Prerequisites Modules 9-10

Learning Objectives

By the end of this module, you will be able to: - Evaluate and select appropriate forex/futures brokers - Execute trades programmatically via OANDA API - Integrate with MetaTrader platforms - Design systems for 24-hour continuous operation

Prerequisites

  • Completed Modules 9 (Risk Management) and 10 (Backtesting)
  • Understanding of order types and position management
  • Familiarity with REST APIs
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import time
import logging

Section 11.1: Broker Selection

Choosing the right broker is critical for live trading success.

Forex Broker Considerations

Key Factors: - Regulation: FCA (UK), NFA/CFTC (US), ASIC (Australia) - Spreads: Variable vs fixed, typical vs minimum - Execution: Market maker vs ECN/STP - API Access: REST, FIX, proprietary - Leverage: Varies by jurisdiction (50:1 US, 30:1 EU)

Popular Forex Brokers with API: - OANDA (REST API, well-documented) - Interactive Brokers (comprehensive but complex) - IG Markets (REST API) - Saxo Bank (OpenAPI)

@dataclass
class BrokerProfile:
    """Broker evaluation profile."""
    name: str
    regulation: List[str]
    typical_spread_eurusd: float  # in pips
    min_deposit: float
    max_leverage: int
    api_type: str
    commission_per_lot: float = 0.0
    
    def total_cost_per_trade(self, lot_size: float = 1.0) -> float:
        """Calculate total cost including spread and commission."""
        spread_cost = self.typical_spread_eurusd * 10 * lot_size  # $10 per pip per lot
        commission = self.commission_per_lot * lot_size * 2  # round trip
        return spread_cost + commission


class BrokerComparator:
    """Compare brokers for trading suitability."""
    
    def __init__(self):
        self.brokers: List[BrokerProfile] = []
        
    def add_broker(self, broker: BrokerProfile):
        """Add broker to comparison."""
        self.brokers.append(broker)
        
    def compare_costs(self, monthly_lots: float = 10.0) -> pd.DataFrame:
        """Compare monthly trading costs."""
        data = []
        for broker in self.brokers:
            cost_per_trade = broker.total_cost_per_trade(1.0)
            monthly_cost = cost_per_trade * monthly_lots
            data.append({
                'Broker': broker.name,
                'Spread (pips)': broker.typical_spread_eurusd,
                'Commission/lot': broker.commission_per_lot,
                'Cost per lot': cost_per_trade,
                f'Monthly ({monthly_lots} lots)': monthly_cost
            })
        return pd.DataFrame(data).sort_values(f'Monthly ({monthly_lots} lots)')
    
    def filter_by_regulation(self, required: List[str]) -> List[BrokerProfile]:
        """Filter brokers by regulation."""
        return [
            b for b in self.brokers 
            if any(reg in b.regulation for reg in required)
        ]
# Example broker comparison
comparator = BrokerComparator()

comparator.add_broker(BrokerProfile(
    name="OANDA",
    regulation=["FCA", "NFA", "ASIC"],
    typical_spread_eurusd=1.0,
    min_deposit=0,
    max_leverage=50,
    api_type="REST"
))

comparator.add_broker(BrokerProfile(
    name="Interactive Brokers",
    regulation=["SEC", "FCA", "ASIC"],
    typical_spread_eurusd=0.1,
    min_deposit=0,
    max_leverage=50,
    api_type="TWS/REST",
    commission_per_lot=2.0
))

comparator.add_broker(BrokerProfile(
    name="ECN Broker",
    regulation=["FCA"],
    typical_spread_eurusd=0.2,
    min_deposit=1000,
    max_leverage=30,
    api_type="FIX",
    commission_per_lot=3.5
))

print("Broker Cost Comparison (10 lots/month):")
print(comparator.compare_costs(10.0))

Futures Broker Considerations

Futures brokers have different characteristics: - Exchange access: CME, ICE, Eurex - Commission structure: Per-contract fees - Margin rates: Day trading vs overnight - Platform: CQG, Rithmic, TT

@dataclass 
class FuturesBrokerProfile:
    """Futures broker profile."""
    name: str
    exchanges: List[str]
    commission_per_contract: float
    platform: str
    day_margin_es: float  # E-mini S&P day margin
    overnight_margin_es: float
    
    def annual_cost(self, contracts_per_day: int, trading_days: int = 252) -> float:
        """Calculate annual commission costs."""
        return self.commission_per_contract * contracts_per_day * trading_days * 2  # round trip


# Compare futures brokers
futures_brokers = [
    FuturesBrokerProfile("AMP Futures", ["CME", "ICE"], 0.59, "Various", 500, 13200),
    FuturesBrokerProfile("NinjaTrader", ["CME", "ICE"], 0.53, "NinjaTrader", 500, 13200),
    FuturesBrokerProfile("Interactive Brokers", ["CME", "ICE", "Eurex"], 0.85, "TWS", 500, 13200),
]

print("Futures Broker Annual Costs (10 contracts/day):")
for broker in futures_brokers:
    annual = broker.annual_cost(10)
    print(f"  {broker.name}: ${annual:,.0f}")

Section 11.2: OANDA Order Execution

OANDA provides a well-documented REST API for forex trading.

class OrderType(Enum):
    MARKET = "MARKET"
    LIMIT = "LIMIT"
    STOP = "STOP"
    MARKET_IF_TOUCHED = "MARKET_IF_TOUCHED"


class OrderSide(Enum):
    BUY = "BUY"
    SELL = "SELL"


@dataclass
class OrderRequest:
    """Order request structure."""
    instrument: str
    units: int  # positive for buy, negative for sell
    order_type: OrderType
    price: Optional[float] = None
    stop_loss: Optional[float] = None
    take_profit: Optional[float] = None
    trailing_stop_distance: Optional[float] = None
    time_in_force: str = "GTC"  # Good Till Cancelled
    
    def to_api_format(self) -> Dict:
        """Convert to OANDA API format."""
        order = {
            "type": self.order_type.value,
            "instrument": self.instrument,
            "units": str(self.units),
            "timeInForce": self.time_in_force
        }
        
        if self.price and self.order_type != OrderType.MARKET:
            order["price"] = str(self.price)
            
        if self.stop_loss:
            order["stopLossOnFill"] = {"price": str(self.stop_loss)}
            
        if self.take_profit:
            order["takeProfitOnFill"] = {"price": str(self.take_profit)}
            
        if self.trailing_stop_distance:
            order["trailingStopLossOnFill"] = {
                "distance": str(self.trailing_stop_distance)
            }
            
        return {"order": order}
@dataclass
class Position:
    """Trading position."""
    instrument: str
    units: int
    average_price: float
    unrealized_pnl: float = 0.0
    
    @property
    def side(self) -> str:
        return "LONG" if self.units > 0 else "SHORT"


class OANDAClient:
    """Simulated OANDA API client for demonstration."""
    
    def __init__(self, account_id: str, practice: bool = True):
        self.account_id = account_id
        self.practice = practice
        self.base_url = "https://api-fxpractice.oanda.com" if practice else "https://api-fxtrade.oanda.com"
        
        # Simulated state
        self._balance = 100000.0
        self._positions: Dict[str, Position] = {}
        self._order_id = 1000
        self._prices = {
            "EUR_USD": (1.0850, 1.0852),  # bid, ask
            "GBP_USD": (1.2650, 1.2653),
            "USD_JPY": (149.50, 149.53),
        }
        
    def get_prices(self, instruments: List[str]) -> Dict[str, Tuple[float, float]]:
        """Get current bid/ask prices."""
        return {inst: self._prices.get(inst, (0, 0)) for inst in instruments}
    
    def get_account(self) -> Dict:
        """Get account summary."""
        unrealized = sum(p.unrealized_pnl for p in self._positions.values())
        return {
            "account_id": self.account_id,
            "balance": self._balance,
            "unrealized_pnl": unrealized,
            "nav": self._balance + unrealized,
            "margin_used": sum(abs(p.units) * p.average_price / 50 for p in self._positions.values()),
            "positions": len(self._positions)
        }
    
    def submit_order(self, order: OrderRequest) -> Dict:
        """Submit an order."""
        self._order_id += 1
        
        # Get fill price
        bid, ask = self._prices.get(order.instrument, (0, 0))
        fill_price = ask if order.units > 0 else bid
        
        # Update position
        if order.instrument in self._positions:
            pos = self._positions[order.instrument]
            new_units = pos.units + order.units
            if new_units == 0:
                del self._positions[order.instrument]
            else:
                pos.units = new_units
                pos.average_price = fill_price
        else:
            self._positions[order.instrument] = Position(
                instrument=order.instrument,
                units=order.units,
                average_price=fill_price
            )
            
        return {
            "order_id": str(self._order_id),
            "status": "FILLED",
            "fill_price": fill_price,
            "units": order.units,
            "instrument": order.instrument
        }
    
    def get_positions(self) -> List[Position]:
        """Get all open positions."""
        return list(self._positions.values())
    
    def close_position(self, instrument: str) -> Dict:
        """Close a position."""
        if instrument not in self._positions:
            return {"error": "No position found"}
            
        pos = self._positions[instrument]
        close_order = OrderRequest(
            instrument=instrument,
            units=-pos.units,
            order_type=OrderType.MARKET
        )
        return self.submit_order(close_order)
# Demonstrate OANDA client usage
client = OANDAClient("101-001-12345678-001", practice=True)

# Check account
print("Account Summary:")
account = client.get_account()
for key, value in account.items():
    print(f"  {key}: {value}")

# Get prices
print("\nCurrent Prices:")
prices = client.get_prices(["EUR_USD", "GBP_USD"])
for inst, (bid, ask) in prices.items():
    print(f"  {inst}: {bid}/{ask} (spread: {(ask-bid)*10000:.1f} pips)")
# Submit a market order with stop loss and take profit
order = OrderRequest(
    instrument="EUR_USD",
    units=10000,  # 0.1 lot long
    order_type=OrderType.MARKET,
    stop_loss=1.0800,
    take_profit=1.0950
)

print("Order Request (API format):")
print(order.to_api_format())

print("\nSubmitting order...")
result = client.submit_order(order)
print(f"Result: {result}")

print("\nOpen Positions:")
for pos in client.get_positions():
    print(f"  {pos.instrument}: {pos.units} @ {pos.average_price} ({pos.side})")

Section 11.3: MetaTrader Integration

MetaTrader 4/5 are popular retail trading platforms with Python integration options.

MetaTrader Overview

MT4 vs MT5: - MT4: Older, forex-focused, MQL4 language - MT5: Newer, multi-asset, MQL5, better Python support

Python Integration Methods: 1. MetaTrader5 package: Official Python library (MT5 only) 2. ZeroMQ bridge: Connect via Expert Advisor 3. File-based: Read/write files for communication 4. REST bridge: Third-party REST API wrappers

class MT5Simulator:
    """Simulated MetaTrader 5 interface."""
    
    def __init__(self):
        self._initialized = False
        self._account_info = {
            'login': 12345678,
            'server': 'Demo-Server',
            'balance': 100000.0,
            'equity': 100000.0,
            'margin': 0.0,
            'margin_free': 100000.0,
            'leverage': 100
        }
        self._positions = []
        self._ticket = 1000000
        
        # Simulated symbols
        self._symbols = {
            'EURUSD': {'bid': 1.0850, 'ask': 1.0852, 'point': 0.00001, 'digits': 5},
            'GBPUSD': {'bid': 1.2650, 'ask': 1.2653, 'point': 0.00001, 'digits': 5},
            'USDJPY': {'bid': 149.50, 'ask': 149.53, 'point': 0.001, 'digits': 3},
        }
        
    def initialize(self, path: str = None) -> bool:
        """Initialize connection to MT5."""
        self._initialized = True
        print(f"MT5 initialized (simulated)")
        return True
    
    def shutdown(self):
        """Shutdown MT5 connection."""
        self._initialized = False
        
    def account_info(self) -> Dict:
        """Get account information."""
        if not self._initialized:
            return None
        return self._account_info
    
    def symbol_info_tick(self, symbol: str) -> Dict:
        """Get current tick for symbol."""
        if symbol not in self._symbols:
            return None
        info = self._symbols[symbol]
        return {
            'symbol': symbol,
            'bid': info['bid'],
            'ask': info['ask'],
            'time': datetime.now()
        }
    
    def order_send(self, request: Dict) -> Dict:
        """Send a trading order."""
        if not self._initialized:
            return {'retcode': -1, 'comment': 'Not initialized'}
            
        self._ticket += 1
        symbol = request.get('symbol')
        volume = request.get('volume', 0.1)
        order_type = request.get('type', 0)  # 0=BUY, 1=SELL
        
        tick = self.symbol_info_tick(symbol)
        if not tick:
            return {'retcode': -1, 'comment': 'Invalid symbol'}
            
        price = tick['ask'] if order_type == 0 else tick['bid']
        
        return {
            'retcode': 10009,  # TRADE_RETCODE_DONE
            'order': self._ticket,
            'volume': volume,
            'price': price,
            'comment': 'Order executed'
        }
    
    def positions_get(self, symbol: str = None) -> List[Dict]:
        """Get open positions."""
        return self._positions
class MT5Trader:
    """High-level MT5 trading interface."""
    
    # Order type constants
    ORDER_TYPE_BUY = 0
    ORDER_TYPE_SELL = 1
    ORDER_TYPE_BUY_LIMIT = 2
    ORDER_TYPE_SELL_LIMIT = 3
    ORDER_TYPE_BUY_STOP = 4
    ORDER_TYPE_SELL_STOP = 5
    
    def __init__(self):
        self.mt5 = MT5Simulator()
        self._connected = False
        
    def connect(self, path: str = None) -> bool:
        """Connect to MetaTrader 5."""
        if self.mt5.initialize(path):
            self._connected = True
            account = self.mt5.account_info()
            print(f"Connected to account {account['login']} on {account['server']}")
            print(f"Balance: ${account['balance']:,.2f}, Leverage: 1:{account['leverage']}")
            return True
        return False
    
    def disconnect(self):
        """Disconnect from MT5."""
        self.mt5.shutdown()
        self._connected = False
        
    def get_price(self, symbol: str) -> Tuple[float, float]:
        """Get bid/ask price."""
        tick = self.mt5.symbol_info_tick(symbol)
        if tick:
            return tick['bid'], tick['ask']
        return None, None
    
    def buy(self, symbol: str, volume: float, sl: float = None, tp: float = None) -> Dict:
        """Place a buy order."""
        bid, ask = self.get_price(symbol)
        if not ask:
            return {'error': 'Could not get price'}
            
        request = {
            'action': 1,  # TRADE_ACTION_DEAL
            'symbol': symbol,
            'volume': volume,
            'type': self.ORDER_TYPE_BUY,
            'price': ask,
            'deviation': 10,
            'magic': 123456,
            'comment': 'Python MT5 order',
        }
        
        if sl:
            request['sl'] = sl
        if tp:
            request['tp'] = tp
            
        return self.mt5.order_send(request)
    
    def sell(self, symbol: str, volume: float, sl: float = None, tp: float = None) -> Dict:
        """Place a sell order."""
        bid, ask = self.get_price(symbol)
        if not bid:
            return {'error': 'Could not get price'}
            
        request = {
            'action': 1,
            'symbol': symbol,
            'volume': volume,
            'type': self.ORDER_TYPE_SELL,
            'price': bid,
            'deviation': 10,
            'magic': 123456,
            'comment': 'Python MT5 order',
        }
        
        if sl:
            request['sl'] = sl
        if tp:
            request['tp'] = tp
            
        return self.mt5.order_send(request)
# Demonstrate MT5 trading
trader = MT5Trader()

if trader.connect():
    # Get price
    symbol = "EURUSD"
    bid, ask = trader.get_price(symbol)
    print(f"\n{symbol}: Bid={bid}, Ask={ask}")
    
    # Place buy order
    result = trader.buy(
        symbol=symbol,
        volume=0.1,
        sl=bid - 0.0050,  # 50 pip stop
        tp=bid + 0.0100   # 100 pip target
    )
    print(f"\nBuy order result: {result}")
    
    trader.disconnect()

Section 11.4: Continuous Operation

Forex markets trade 24 hours, requiring robust systems for continuous operation.

class TradingSession(Enum):
    """Global trading sessions."""
    SYDNEY = "Sydney"
    TOKYO = "Tokyo"
    LONDON = "London"
    NEW_YORK = "New York"


class SessionMonitor:
    """Monitor trading sessions and market hours."""
    
    # Session times in UTC
    SESSIONS = {
        TradingSession.SYDNEY: (21, 6),    # 21:00 - 06:00 UTC
        TradingSession.TOKYO: (0, 9),      # 00:00 - 09:00 UTC
        TradingSession.LONDON: (7, 16),    # 07:00 - 16:00 UTC
        TradingSession.NEW_YORK: (12, 21), # 12:00 - 21:00 UTC
    }
    
    def get_active_sessions(self, utc_hour: int = None) -> List[TradingSession]:
        """Get currently active trading sessions."""
        if utc_hour is None:
            utc_hour = datetime.utcnow().hour
            
        active = []
        for session, (start, end) in self.SESSIONS.items():
            if start < end:
                if start <= utc_hour < end:
                    active.append(session)
            else:  # Crosses midnight
                if utc_hour >= start or utc_hour < end:
                    active.append(session)
        return active
    
    def is_weekend(self, dt: datetime = None) -> bool:
        """Check if market is closed for weekend."""
        if dt is None:
            dt = datetime.utcnow()
        
        # Forex closes Friday 21:00 UTC, opens Sunday 21:00 UTC
        if dt.weekday() == 5:  # Saturday
            return True
        if dt.weekday() == 6 and dt.hour < 21:  # Sunday before open
            return True
        if dt.weekday() == 4 and dt.hour >= 21:  # Friday after close
            return True
        return False
    
    def get_session_overlap(self, utc_hour: int = None) -> str:
        """Identify high-liquidity session overlaps."""
        active = self.get_active_sessions(utc_hour)
        
        if TradingSession.LONDON in active and TradingSession.NEW_YORK in active:
            return "London-NY Overlap (Highest Liquidity)"
        elif TradingSession.TOKYO in active and TradingSession.LONDON in active:
            return "Tokyo-London Overlap"
        elif TradingSession.SYDNEY in active and TradingSession.TOKYO in active:
            return "Sydney-Tokyo Overlap"
        elif len(active) == 1:
            return f"{active[0].value} Session Only"
        return "Low Liquidity Period"
# Demonstrate session monitoring
monitor = SessionMonitor()

print("Session Activity by Hour (UTC):")
print("-" * 60)

for hour in range(0, 24, 3):
    sessions = monitor.get_active_sessions(hour)
    overlap = monitor.get_session_overlap(hour)
    session_names = [s.value for s in sessions]
    print(f"{hour:02d}:00 - {session_names} - {overlap}")
class SystemHealth:
    """Monitor trading system health."""
    
    def __init__(self):
        self.checks: Dict[str, bool] = {}
        self.last_check = None
        self.errors: List[str] = []
        
    def check_broker_connection(self, client) -> bool:
        """Verify broker connection is active."""
        try:
            account = client.get_account()
            self.checks['broker_connection'] = account is not None
            return self.checks['broker_connection']
        except Exception as e:
            self.errors.append(f"Broker connection error: {e}")
            self.checks['broker_connection'] = False
            return False
    
    def check_data_feed(self, client, symbols: List[str]) -> bool:
        """Verify price data is flowing."""
        try:
            prices = client.get_prices(symbols)
            valid = all(prices.get(s, (0, 0))[0] > 0 for s in symbols)
            self.checks['data_feed'] = valid
            return valid
        except Exception as e:
            self.errors.append(f"Data feed error: {e}")
            self.checks['data_feed'] = False
            return False
    
    def check_margin_level(self, client, min_level: float = 200.0) -> bool:
        """Check margin level is healthy."""
        try:
            account = client.get_account()
            margin_used = account.get('margin_used', 0)
            if margin_used > 0:
                margin_level = (account['nav'] / margin_used) * 100
            else:
                margin_level = float('inf')
            self.checks['margin_level'] = margin_level >= min_level
            return self.checks['margin_level']
        except Exception as e:
            self.errors.append(f"Margin check error: {e}")
            self.checks['margin_level'] = False
            return False
    
    def run_all_checks(self, client, symbols: List[str]) -> Dict[str, bool]:
        """Run all health checks."""
        self.errors = []
        self.check_broker_connection(client)
        self.check_data_feed(client, symbols)
        self.check_margin_level(client)
        self.last_check = datetime.now()
        return self.checks
    
    def is_healthy(self) -> bool:
        """Check if all systems are healthy."""
        return all(self.checks.values())
    
    def get_status_report(self) -> str:
        """Generate status report."""
        lines = ["System Health Report", "=" * 30]
        for check, status in self.checks.items():
            icon = "OK" if status else "FAIL"
            lines.append(f"{check}: {icon}")
        if self.errors:
            lines.append("\nErrors:")
            for error in self.errors:
                lines.append(f"  - {error}")
        return "\n".join(lines)
# Demonstrate health monitoring
health = SystemHealth()
client = OANDAClient("101-001-12345678-001")

results = health.run_all_checks(client, ["EUR_USD", "GBP_USD"])
print(health.get_status_report())
print(f"\nSystem healthy: {health.is_healthy()}")
class TradingBot:
    """24-hour trading bot framework."""
    
    def __init__(self, client, strategy):
        self.client = client
        self.strategy = strategy
        self.health = SystemHealth()
        self.session_monitor = SessionMonitor()
        self.running = False
        self.trades_today = 0
        self.max_daily_trades = 10
        
        # Configure logging
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger('TradingBot')
        
    def pre_trade_checks(self) -> bool:
        """Run checks before each trading cycle."""
        # Check if weekend
        if self.session_monitor.is_weekend():
            self.logger.info("Market closed for weekend")
            return False
            
        # Check daily trade limit
        if self.trades_today >= self.max_daily_trades:
            self.logger.warning("Daily trade limit reached")
            return False
            
        # Run health checks
        self.health.run_all_checks(self.client, ["EUR_USD"])
        if not self.health.is_healthy():
            self.logger.error("Health check failed")
            return False
            
        return True
    
    def run_cycle(self) -> Optional[Dict]:
        """Run one trading cycle."""
        if not self.pre_trade_checks():
            return None
            
        # Get market data
        prices = self.client.get_prices(["EUR_USD"])
        
        # Get signal from strategy
        signal = self.strategy.generate_signal(prices)
        
        if signal:
            self.logger.info(f"Signal generated: {signal}")
            # Execute signal
            result = self.execute_signal(signal)
            if result and result.get('status') == 'FILLED':
                self.trades_today += 1
            return result
            
        return None
    
    def execute_signal(self, signal: Dict) -> Dict:
        """Execute a trading signal."""
        order = OrderRequest(
            instrument=signal['instrument'],
            units=signal['units'],
            order_type=OrderType.MARKET,
            stop_loss=signal.get('stop_loss'),
            take_profit=signal.get('take_profit')
        )
        return self.client.submit_order(order)
    
    def reset_daily_counters(self):
        """Reset counters at start of new day."""
        self.trades_today = 0
        self.logger.info("Daily counters reset")
# Simple strategy for demonstration
class SimpleStrategy:
    """Simple demonstration strategy."""
    
    def __init__(self, instrument: str = "EUR_USD"):
        self.instrument = instrument
        self.signal_count = 0
        
    def generate_signal(self, prices: Dict) -> Optional[Dict]:
        """Generate trading signal (simulated)."""
        # In real implementation, this would contain actual strategy logic
        self.signal_count += 1
        
        # Only generate signal occasionally for demo
        if self.signal_count % 5 == 0:
            bid, ask = prices.get(self.instrument, (0, 0))
            return {
                'instrument': self.instrument,
                'units': 10000,
                'stop_loss': bid - 0.0050,
                'take_profit': bid + 0.0100
            }
        return None


# Demonstrate bot framework
client = OANDAClient("101-001-12345678-001")
strategy = SimpleStrategy("EUR_USD")
bot = TradingBot(client, strategy)

print("Running 3 trading cycles:")
for i in range(3):
    print(f"\nCycle {i+1}:")
    result = bot.run_cycle()
    if result:
        print(f"  Trade executed: {result}")
    else:
        print("  No trade")

Exercises

Exercise 1: Broker Comparison (Guided)

Complete the broker scoring system.

class BrokerScorer:
    """Score brokers based on multiple criteria."""
    
    def __init__(self):
        # Weights for different criteria
        self.weights = {
            'cost': 0.30,
            'regulation': 0.25,
            'api_quality': 0.25,
            'leverage': 0.20
        }
        
    def score_cost(self, spread_pips: float, commission: float) -> float:
        """Score cost (lower is better). Returns 0-100."""
        total_cost = spread_pips * 10 + commission * 2
        # Assume $50 is worst case, $5 is best case
        score = max(0, 100 - (total_cost - 5) * (100 / 45))
        return ______  # Return the score
    
    def score_regulation(self, regulations: List[str]) -> float:
        """Score regulation quality. Returns 0-100."""
        tier1 = ['FCA', 'NFA', 'ASIC', 'SEC']
        tier2 = ['CySEC', 'FINMA', 'BaFin']
        
        score = 0
        for reg in regulations:
            if reg in tier1:
                score += 40
            elif reg in tier2:
                score += 25
        return ______(score, 100)  # Cap at 100
    
    def score_api(self, api_type: str) -> float:
        """Score API quality. Returns 0-100."""
        api_scores = {
            'REST': 90,
            'FIX': 95,
            'TWS': 70,
            'Proprietary': 50
        }
        return api_scores.get(______, 30)  # Get score or default 30
    
    def calculate_total_score(self, broker: BrokerProfile) -> float:
        """Calculate weighted total score."""
        cost_score = self.score_cost(broker.typical_spread_eurusd, broker.commission_per_lot)
        reg_score = self.score_regulation(broker.regulation)
        api_score = self.score_api(broker.api_type)
        leverage_score = min(broker.max_leverage * 2, 100)
        
        total = (
            cost_score * self.weights['cost'] +
            reg_score * self.weights['regulation'] +
            api_score * self.weights['api_quality'] +
            leverage_score * self.weights['______']  # Fill in weight key
        )
        return round(total, 1)


# Test the scorer
scorer = BrokerScorer()
test_broker = BrokerProfile(
    name="Test Broker",
    regulation=["FCA", "ASIC"],
    typical_spread_eurusd=1.0,
    min_deposit=0,
    max_leverage=50,
    api_type="REST"
)

score = scorer.calculate_total_score(test_broker)
print(f"Broker score: {score}/100")

Exercise 2: Order Builder (Guided)

Complete the order builder with validation.

class OrderBuilder:
    """Builder pattern for creating validated orders."""
    
    def __init__(self):
        self._instrument: str = None
        self._units: int = None
        self._order_type: OrderType = OrderType.MARKET
        self._price: float = None
        self._stop_loss: float = None
        self._take_profit: float = None
        self._errors: List[str] = []
        
    def instrument(self, instrument: str) -> 'OrderBuilder':
        """Set instrument."""
        valid_instruments = ['EUR_USD', 'GBP_USD', 'USD_JPY', 'AUD_USD']
        if instrument not in valid_instruments:
            self._errors.append(f"Invalid instrument: {instrument}")
        self._instrument = ______  # Set the instrument
        return self
    
    def units(self, units: int) -> 'OrderBuilder':
        """Set position size (positive=buy, negative=sell)."""
        if abs(units) < 1000:
            self._errors.append("Minimum order size is 1000 units")
        if abs(units) > 10000000:
            self._errors.append("Maximum order size is 10M units")
        self.______ = units  # Set units attribute
        return self
    
    def limit_order(self, price: float) -> 'OrderBuilder':
        """Set as limit order."""
        self._order_type = OrderType.LIMIT
        self._price = price
        return self
    
    def stop_loss(self, price: float) -> 'OrderBuilder':
        """Set stop loss."""
        self._stop_loss = ______  # Set stop loss price
        return self
    
    def take_profit(self, price: float) -> 'OrderBuilder':
        """Set take profit."""
        self._take_profit = price
        return self
    
    def validate(self) -> bool:
        """Validate order configuration."""
        if not self._instrument:
            self._errors.append("Instrument is required")
        if not self._units:
            self._errors.append("Units is required")
            
        # Validate stop loss direction
        if self._stop_loss and self._units:
            if self._units > 0 and self._stop_loss >= self._price if self._price else False:
                self._errors.append("Stop loss must be below entry for long positions")
                
        return len(self._errors) == ______  # Return True if no errors
    
    def build(self) -> OrderRequest:
        """Build and return the order."""
        if not self.validate():
            raise ValueError(f"Invalid order: {self._errors}")
            
        return OrderRequest(
            instrument=self._instrument,
            units=self._units,
            order_type=self._order_type,
            price=self._price,
            stop_loss=self._stop_loss,
            take_profit=self._take_profit
        )


# Test the builder
try:
    order = (
        OrderBuilder()
        .instrument("EUR_USD")
        .units(10000)
        .stop_loss(1.0800)
        .take_profit(1.0950)
        .build()
    )
    print(f"Order created: {order}")
except ValueError as e:
    print(f"Error: {e}")

Exercise 3: Connection Manager (Guided)

Complete the connection manager with reconnection logic.

class ConnectionManager:
    """Manage broker connection with auto-reconnect."""
    
    def __init__(self, client, max_retries: int = 3, retry_delay: float = 5.0):
        self.client = client
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        self.connected = False
        self.connection_attempts = 0
        self.last_error: str = None
        
    def connect(self) -> bool:
        """Attempt to connect with retries."""
        self.connection_attempts = 0
        
        while self.connection_attempts < self.______:  # Check max retries
            self.connection_attempts += 1
            print(f"Connection attempt {self.connection_attempts}/{self.max_retries}")
            
            try:
                # Simulate connection (would call real API)
                account = self.client.get_account()
                if account:
                    self.connected = ______  # Set connected status
                    print("Connected successfully")
                    return True
            except Exception as e:
                self.last_error = str(e)
                print(f"Connection failed: {e}")
                
            if self.connection_attempts < self.max_retries:
                print(f"Retrying in {self.retry_delay} seconds...")
                time.sleep(self.______)  # Wait before retry
                
        self.connected = False
        return False
    
    def ensure_connected(self) -> bool:
        """Ensure connection is active, reconnect if needed."""
        if self.connected:
            # Verify connection with heartbeat
            try:
                self.client.get_account()
                return True
            except:
                self.connected = False
                
        return self.connect()


# Test connection manager
client = OANDAClient("test-account")
manager = ConnectionManager(client, max_retries=3, retry_delay=0.1)

if manager.connect():
    print("\nConnection established")
    print(f"Total attempts: {manager.connection_attempts}")

Exercise 4: Deployment Checklist

Create a comprehensive pre-deployment checklist system for a live trading bot.

# Exercise 4: Build a DeploymentChecklist class that:
# 1. Defines critical checks (broker connection, risk params, strategy validation)
# 2. Defines warning checks (time until weekend, market hours)
# 3. Has a run_all() method that executes all checks
# 4. Returns a report showing pass/fail status and any warnings

# Your code here

Exercise 5: Alert System

Build an alert system for monitoring live trades.

# Exercise 5: Build an AlertSystem class that:
# 1. Monitors positions for stop loss proximity (within X pips)
# 2. Monitors daily P&L against thresholds (max loss, target profit)
# 3. Monitors margin level
# 4. Generates alerts with severity levels (INFO, WARNING, CRITICAL)
# 5. Has a method to get all active alerts

# Your code here

Exercise 6: Error Recovery

Implement error handling and recovery for common trading issues.

# Exercise 6: Build an ErrorRecovery class that:
# 1. Handles order rejection (retry with adjusted parameters)
# 2. Handles connection loss (attempt reconnection)
# 3. Handles position reconciliation (compare local vs broker positions)
# 4. Logs all errors and recovery attempts
# 5. Implements circuit breaker pattern (disable trading after N errors)

# Your code here

Module Project: Live Trading System

Build a complete live trading system framework.

class LiveTradingSystem:
    """
    Complete live trading system.
    
    Integrates:
    - Broker connection with auto-reconnect
    - Session monitoring
    - Health checks
    - Order management
    - Alerting
    - Logging
    """
    
    def __init__(self, config: Dict):
        """
        Initialize trading system.
        
        Config should include:
        - broker: Broker client instance
        - strategy: Trading strategy instance
        - risk_params: Risk management parameters
        - instruments: List of instruments to trade
        """
        self.config = config
        self.client = config['broker']
        self.strategy = config['strategy']
        self.instruments = config.get('instruments', ['EUR_USD'])
        
        # Components
        self.session_monitor = SessionMonitor()
        self.health = SystemHealth()
        
        # State
        self.running = False
        self.paused = False
        self.daily_pnl = 0.0
        self.trades_today = 0
        
        # Risk parameters
        self.max_daily_loss = config.get('risk_params', {}).get('max_daily_loss', -1000)
        self.max_daily_trades = config.get('risk_params', {}).get('max_daily_trades', 10)
        
        # Setup logging
        self._setup_logging()
        
    def _setup_logging(self):
        """Configure logging."""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )
        self.logger = logging.getLogger('LiveTradingSystem')
        
    def startup_checks(self) -> bool:
        """Run all startup checks."""
        self.logger.info("Running startup checks...")
        
        # Check broker connection
        if not self.health.check_broker_connection(self.client):
            self.logger.error("Broker connection failed")
            return False
            
        # Check data feed
        if not self.health.check_data_feed(self.client, self.instruments):
            self.logger.error("Data feed check failed")
            return False
            
        # Check margin
        if not self.health.check_margin_level(self.client):
            self.logger.error("Margin level check failed")
            return False
            
        self.logger.info("All startup checks passed")
        return True
    
    def can_trade(self) -> Tuple[bool, str]:
        """Check if trading is allowed."""
        # Check if paused
        if self.paused:
            return False, "System is paused"
            
        # Check weekend
        if self.session_monitor.is_weekend():
            return False, "Market closed for weekend"
            
        # Check daily loss limit
        if self.daily_pnl <= self.max_daily_loss:
            return False, f"Daily loss limit reached: {self.daily_pnl}"
            
        # Check daily trade limit
        if self.trades_today >= self.max_daily_trades:
            return False, "Daily trade limit reached"
            
        return True, "OK"
    
    def process_signal(self, signal: Dict) -> Optional[Dict]:
        """Process and execute a trading signal."""
        can_trade, reason = self.can_trade()
        if not can_trade:
            self.logger.warning(f"Cannot trade: {reason}")
            return None
            
        # Create order
        order = OrderRequest(
            instrument=signal['instrument'],
            units=signal['units'],
            order_type=OrderType.MARKET,
            stop_loss=signal.get('stop_loss'),
            take_profit=signal.get('take_profit')
        )
        
        # Execute
        self.logger.info(f"Executing order: {signal['instrument']} {signal['units']} units")
        result = self.client.submit_order(order)
        
        if result.get('status') == 'FILLED':
            self.trades_today += 1
            self.logger.info(f"Order filled: {result}")
        else:
            self.logger.warning(f"Order not filled: {result}")
            
        return result
    
    def run_cycle(self):
        """Run one trading cycle."""
        # Health check
        if not self.health.is_healthy():
            self.health.run_all_checks(self.client, self.instruments)
            if not self.health.is_healthy():
                self.logger.warning("Health check failed, skipping cycle")
                return
                
        # Get prices
        prices = self.client.get_prices(self.instruments)
        
        # Generate signal
        signal = self.strategy.generate_signal(prices)
        
        if signal:
            self.process_signal(signal)
    
    def pause(self):
        """Pause trading."""
        self.paused = True
        self.logger.info("Trading paused")
        
    def resume(self):
        """Resume trading."""
        self.paused = False
        self.logger.info("Trading resumed")
        
    def shutdown(self):
        """Graceful shutdown."""
        self.logger.info("Initiating shutdown...")
        self.running = False
        
        # Close all positions
        positions = self.client.get_positions()
        for pos in positions:
            self.logger.info(f"Closing position: {pos.instrument}")
            self.client.close_position(pos.instrument)
            
        self.logger.info("Shutdown complete")
        
    def get_status(self) -> Dict:
        """Get current system status."""
        account = self.client.get_account()
        sessions = self.session_monitor.get_active_sessions()
        
        return {
            'running': self.running,
            'paused': self.paused,
            'healthy': self.health.is_healthy(),
            'daily_pnl': self.daily_pnl,
            'trades_today': self.trades_today,
            'active_sessions': [s.value for s in sessions],
            'balance': account.get('balance', 0),
            'positions': account.get('positions', 0)
        }
# Demonstrate the live trading system
config = {
    'broker': OANDAClient("101-001-12345678-001"),
    'strategy': SimpleStrategy("EUR_USD"),
    'instruments': ['EUR_USD', 'GBP_USD'],
    'risk_params': {
        'max_daily_loss': -500,
        'max_daily_trades': 5
    }
}

system = LiveTradingSystem(config)

# Run startup checks
if system.startup_checks():
    print("\nSystem Status:")
    status = system.get_status()
    for key, value in status.items():
        print(f"  {key}: {value}")
        
    # Run a few cycles
    print("\nRunning trading cycles:")
    for i in range(3):
        system.run_cycle()
        
    # Check final status
    print(f"\nTrades executed: {system.trades_today}")

Key Takeaways

  1. Broker Selection: Evaluate brokers on regulation, costs, API quality, and execution

  2. Order Execution: Use proper order types with stop loss and take profit for risk management

  3. Platform Integration: OANDA REST API and MetaTrader 5 offer different trade-offs for Python integration

  4. 24-Hour Operation: Forex requires session awareness, health monitoring, and automatic recovery

  5. System Design: Production systems need connection management, health checks, alerting, and graceful shutdown

  6. Risk Controls: Always implement daily loss limits, trade limits, and margin monitoring


Next Module: Module 12 - Advanced Strategies (Currency Indices, Spread Trading)

Module 12: Advanced Strategies

Duration ~2.5 hours
Skill Level Advanced
Prerequisites Modules 8-11

Learning Objectives

By the end of this module, you will be able to: - Build and trade custom currency indices - Implement spread and pairs trading strategies - Understand options on forex and futures - Identify and trade seasonal patterns

Prerequisites

  • Completed Part 3 (Risk & Execution)
  • Understanding of technical and fundamental analysis
  • Familiarity with backtesting concepts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
from scipy import stats

Section 12.1: Currency Indices

Currency indices measure a currency's strength against a basket of currencies.

The US Dollar Index (DXY)

The DXY measures USD against 6 major currencies: - EUR (57.6%) - JPY (13.6%) - GBP (11.9%) - CAD (9.1%) - SEK (4.2%) - CHF (3.6%)

Formula: DXY = 50.14348112 × (EUR/USD)^-0.576 × (USD/JPY)^0.136 × ...

class DollarIndex:
    """Calculate the US Dollar Index (DXY)."""
    
    # Official DXY weights
    WEIGHTS = {
        'EUR': -0.576,   # Negative because EUR/USD
        'JPY': 0.136,    # Positive because USD/JPY
        'GBP': -0.119,   # Negative because GBP/USD
        'CAD': 0.091,    # Positive because USD/CAD
        'SEK': 0.042,    # Positive because USD/SEK
        'CHF': 0.036     # Positive because USD/CHF
    }
    
    CONSTANT = 50.14348112
    
    def calculate(self, rates: Dict[str, float]) -> float:
        """
        Calculate DXY from exchange rates.
        
        rates should contain:
        - EURUSD, USDJPY, GBPUSD, USDCAD, USDSEK, USDCHF
        """
        dxy = self.CONSTANT
        
        # Convert rates to proper format
        rate_map = {
            'EUR': rates.get('EURUSD', 1.0),
            'JPY': rates.get('USDJPY', 100.0),
            'GBP': rates.get('GBPUSD', 1.0),
            'CAD': rates.get('USDCAD', 1.0),
            'SEK': rates.get('USDSEK', 10.0),
            'CHF': rates.get('USDCHF', 1.0)
        }
        
        for currency, weight in self.WEIGHTS.items():
            rate = rate_map[currency]
            dxy *= rate ** weight
            
        return dxy
    
    def calculate_contribution(self, rates: Dict[str, float], 
                               prev_rates: Dict[str, float]) -> Dict[str, float]:
        """Calculate each currency's contribution to DXY change."""
        contributions = {}
        
        rate_map = {
            'EUR': ('EURUSD', -1),
            'JPY': ('USDJPY', 1),
            'GBP': ('GBPUSD', -1),
            'CAD': ('USDCAD', 1),
            'SEK': ('USDSEK', 1),
            'CHF': ('USDCHF', 1)
        }
        
        for currency, (pair, direction) in rate_map.items():
            current = rates.get(pair, 1.0)
            previous = prev_rates.get(pair, 1.0)
            change = (current / previous - 1) * direction
            weight = abs(self.WEIGHTS[currency])
            contributions[currency] = change * weight * 100  # in percentage points
            
        return contributions
# Calculate DXY example
dxy = DollarIndex()

current_rates = {
    'EURUSD': 1.0850,
    'USDJPY': 149.50,
    'GBPUSD': 1.2650,
    'USDCAD': 1.3500,
    'USDSEK': 10.50,
    'USDCHF': 0.8800
}

index_value = dxy.calculate(current_rates)
print(f"DXY Value: {index_value:.2f}")

# Calculate contribution from rate changes
previous_rates = {
    'EURUSD': 1.0900,
    'USDJPY': 148.00,
    'GBPUSD': 1.2700,
    'USDCAD': 1.3450,
    'USDSEK': 10.40,
    'USDCHF': 0.8750
}

contributions = dxy.calculate_contribution(current_rates, previous_rates)
print("\nContributions to DXY change:")
for currency, contrib in sorted(contributions.items(), key=lambda x: abs(x[1]), reverse=True):
    print(f"  {currency}: {contrib:+.3f}%")
class CustomCurrencyIndex:
    """Build custom currency strength indices."""
    
    def __init__(self, base_currency: str, pairs: List[str], weights: Dict[str, float] = None):
        """
        Initialize custom index.
        
        base_currency: Currency to measure strength of (e.g., 'EUR')
        pairs: List of pairs involving this currency
        weights: Optional custom weights (default: equal)
        """
        self.base_currency = base_currency
        self.pairs = pairs
        
        if weights:
            self.weights = weights
        else:
            # Equal weights
            self.weights = {pair: 1.0 / len(pairs) for pair in pairs}
            
    def calculate(self, rates: Dict[str, float], base_value: float = 100.0) -> float:
        """Calculate index value from rates."""
        index = 0.0
        
        for pair, weight in self.weights.items():
            rate = rates.get(pair, 1.0)
            
            # Determine if base currency is first or second in pair
            if pair.startswith(self.base_currency):
                # EUR in EURUSD - higher rate = stronger EUR
                contribution = rate * weight
            else:
                # EUR in GBPEUR - lower rate = stronger EUR
                contribution = (1 / rate) * weight
                
            index += contribution
            
        return index * base_value
    
    def calculate_strength(self, rates_series: pd.DataFrame) -> pd.Series:
        """Calculate index over time series."""
        values = []
        for _, row in rates_series.iterrows():
            rates = row.to_dict()
            values.append(self.calculate(rates))
        return pd.Series(values, index=rates_series.index)
# Create EUR strength index
eur_index = CustomCurrencyIndex(
    base_currency='EUR',
    pairs=['EURUSD', 'EURJPY', 'EURGBP', 'EURCHF'],
    weights={
        'EURUSD': 0.40,  # Most important
        'EURJPY': 0.25,
        'EURGBP': 0.20,
        'EURCHF': 0.15
    }
)

sample_rates = {
    'EURUSD': 1.0850,
    'EURJPY': 162.20,
    'EURGBP': 0.8580,
    'EURCHF': 0.9550
}

eur_strength = eur_index.calculate(sample_rates)
print(f"EUR Strength Index: {eur_strength:.2f}")

Section 12.2: Spread Trading

Spread trading involves simultaneously buying and selling related instruments.

@dataclass
class SpreadTrade:
    """Represents a spread trade."""
    long_instrument: str
    short_instrument: str
    long_units: float
    short_units: float
    entry_spread: float
    current_spread: float = 0.0
    
    @property
    def pnl(self) -> float:
        """Calculate P&L from spread change."""
        return self.current_spread - self.entry_spread


class InterCommoditySpread:
    """Trade spreads between related commodities."""
    
    # Common commodity spreads
    COMMON_SPREADS = {
        'crack_spread': ('CL', 'RB'),     # Crude oil vs Gasoline
        'crush_spread': ('ZS', 'ZM'),     # Soybeans vs Soybean Meal
        'gold_silver': ('GC', 'SI'),       # Gold vs Silver ratio
        'brent_wti': ('BZ', 'CL'),         # Brent vs WTI crude
    }
    
    def __init__(self, spread_type: str):
        if spread_type not in self.COMMON_SPREADS:
            raise ValueError(f"Unknown spread type: {spread_type}")
        self.spread_type = spread_type
        self.long_sym, self.short_sym = self.COMMON_SPREADS[spread_type]
        
    def calculate_spread(self, long_price: float, short_price: float) -> float:
        """Calculate spread value."""
        if self.spread_type == 'gold_silver':
            # Gold/Silver ratio
            return long_price / short_price
        else:
            # Price difference
            return long_price - short_price
    
    def analyze_spread(self, spread_history: pd.Series) -> Dict:
        """Analyze spread statistics."""
        return {
            'current': spread_history.iloc[-1],
            'mean': spread_history.mean(),
            'std': spread_history.std(),
            'z_score': (spread_history.iloc[-1] - spread_history.mean()) / spread_history.std(),
            'percentile': stats.percentileofscore(spread_history, spread_history.iloc[-1]),
            'min': spread_history.min(),
            'max': spread_history.max()
        }
# Generate synthetic gold/silver ratio data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=252, freq='D')

# Gold/Silver ratio typically ranges 60-90
ratio_mean = 75
ratio_std = 5
ratios = ratio_mean + ratio_std * np.cumsum(np.random.randn(252) * 0.1)
spread_series = pd.Series(ratios, index=dates)

# Analyze the spread
gs_spread = InterCommoditySpread('gold_silver')
analysis = gs_spread.analyze_spread(spread_series)

print("Gold/Silver Ratio Analysis:")
for key, value in analysis.items():
    print(f"  {key}: {value:.2f}")

# Plot
plt.figure(figsize=(12, 6))
plt.plot(spread_series, label='Gold/Silver Ratio')
plt.axhline(y=analysis['mean'], color='r', linestyle='--', label=f"Mean: {analysis['mean']:.1f}")
plt.axhline(y=analysis['mean'] + 2*analysis['std'], color='g', linestyle=':', label='+2 Std')
plt.axhline(y=analysis['mean'] - 2*analysis['std'], color='g', linestyle=':', label='-2 Std')
plt.title('Gold/Silver Ratio with Bollinger Bands')
plt.xlabel('Date')
plt.ylabel('Ratio')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
class CalendarSpread:
    """Trade spreads between different contract months."""
    
    def __init__(self, symbol: str, front_month: str, back_month: str):
        self.symbol = symbol
        self.front_month = front_month
        self.back_month = back_month
        
    def calculate_spread(self, front_price: float, back_price: float) -> float:
        """Calculate calendar spread (back - front)."""
        return back_price - front_price
    
    def determine_market_structure(self, front_price: float, back_price: float) -> str:
        """Determine contango or backwardation."""
        spread = self.calculate_spread(front_price, back_price)
        if spread > 0:
            return "Contango"  # Back month more expensive
        elif spread < 0:
            return "Backwardation"  # Front month more expensive
        else:
            return "Flat"
    
    def calculate_roll_yield(self, front_price: float, back_price: float, 
                             days_to_expiry: int) -> float:
        """Calculate annualized roll yield."""
        if front_price == 0 or days_to_expiry == 0:
            return 0.0
        spread_pct = (back_price - front_price) / front_price
        annualized = spread_pct * (365 / days_to_expiry)
        return annualized * 100  # As percentage


# Example: Crude Oil calendar spread
oil_spread = CalendarSpread('CL', 'Mar24', 'Jun24')

front_price = 72.50
back_price = 73.80

spread = oil_spread.calculate_spread(front_price, back_price)
structure = oil_spread.determine_market_structure(front_price, back_price)
roll_yield = oil_spread.calculate_roll_yield(front_price, back_price, 90)

print(f"Crude Oil Calendar Spread:")
print(f"  Front ({oil_spread.front_month}): ${front_price}")
print(f"  Back ({oil_spread.back_month}): ${back_price}")
print(f"  Spread: ${spread:.2f}")
print(f"  Market Structure: {structure}")
print(f"  Annualized Roll Yield: {roll_yield:.2f}%")
class CurrencyPairsTrader:
    """Statistical arbitrage on correlated currency pairs."""
    
    def __init__(self, pair1: str, pair2: str, lookback: int = 60):
        self.pair1 = pair1
        self.pair2 = pair2
        self.lookback = lookback
        self.hedge_ratio = 1.0
        
    def calculate_hedge_ratio(self, prices1: pd.Series, prices2: pd.Series) -> float:
        """Calculate optimal hedge ratio using linear regression."""
        # Use recent prices for regression
        p1 = prices1.iloc[-self.lookback:]
        p2 = prices2.iloc[-self.lookback:]
        
        slope, intercept, r_value, p_value, std_err = stats.linregress(p2, p1)
        self.hedge_ratio = slope
        self.correlation = r_value
        return slope
    
    def calculate_spread(self, price1: float, price2: float) -> float:
        """Calculate spread using hedge ratio."""
        return price1 - self.hedge_ratio * price2
    
    def calculate_spread_series(self, prices1: pd.Series, prices2: pd.Series) -> pd.Series:
        """Calculate spread time series."""
        return prices1 - self.hedge_ratio * prices2
    
    def generate_signals(self, spread: pd.Series, z_threshold: float = 2.0) -> pd.Series:
        """Generate trading signals based on z-score."""
        mean = spread.rolling(self.lookback).mean()
        std = spread.rolling(self.lookback).std()
        z_score = (spread - mean) / std
        
        signals = pd.Series(0, index=spread.index)
        signals[z_score > z_threshold] = -1   # Short spread
        signals[z_score < -z_threshold] = 1   # Long spread
        
        return signals
# Generate synthetic correlated pair data
np.random.seed(42)
n = 252
dates = pd.date_range('2024-01-01', periods=n, freq='D')

# EURUSD and GBPUSD are highly correlated
common_factor = np.cumsum(np.random.randn(n) * 0.001)
eurusd = 1.08 + common_factor + np.cumsum(np.random.randn(n) * 0.0005)
gbpusd = 1.26 + common_factor * 1.1 + np.cumsum(np.random.randn(n) * 0.0006)

eurusd_series = pd.Series(eurusd, index=dates)
gbpusd_series = pd.Series(gbpusd, index=dates)

# Create pairs trader
pairs_trader = CurrencyPairsTrader('EURUSD', 'GBPUSD', lookback=30)
hedge_ratio = pairs_trader.calculate_hedge_ratio(eurusd_series, gbpusd_series)

print(f"Hedge Ratio: {hedge_ratio:.4f}")
print(f"Correlation: {pairs_trader.correlation:.4f}")

# Calculate spread and signals
spread = pairs_trader.calculate_spread_series(eurusd_series, gbpusd_series)
signals = pairs_trader.generate_signals(spread)

print(f"\nSignal distribution:")
print(signals.value_counts())

Section 12.3: Options on Futures

Options provide additional strategic flexibility in forex and futures trading.

class OptionType(Enum):
    CALL = "call"
    PUT = "put"


@dataclass
class FXOption:
    """FX Option representation."""
    pair: str
    option_type: OptionType
    strike: float
    expiry_days: int
    premium: float
    notional: float = 100000  # Standard lot
    
    def intrinsic_value(self, spot: float) -> float:
        """Calculate intrinsic value."""
        if self.option_type == OptionType.CALL:
            return max(0, spot - self.strike) * self.notional
        else:
            return max(0, self.strike - spot) * self.notional
    
    def payoff_at_expiry(self, spot: float) -> float:
        """Calculate P&L at expiry."""
        return self.intrinsic_value(spot) - self.premium
    
    def breakeven(self) -> float:
        """Calculate breakeven price."""
        premium_per_unit = self.premium / self.notional
        if self.option_type == OptionType.CALL:
            return self.strike + premium_per_unit
        else:
            return self.strike - premium_per_unit


class BlackScholes:
    """Black-Scholes option pricing for FX options."""
    
    @staticmethod
    def d1(S: float, K: float, r_d: float, r_f: float, sigma: float, T: float) -> float:
        """Calculate d1."""
        return (np.log(S / K) + (r_d - r_f + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    
    @staticmethod
    def d2(d1: float, sigma: float, T: float) -> float:
        """Calculate d2."""
        return d1 - sigma * np.sqrt(T)
    
    @classmethod
    def price(cls, S: float, K: float, r_d: float, r_f: float, 
              sigma: float, T: float, option_type: OptionType) -> float:
        """
        Calculate option price using Garman-Kohlhagen (FX Black-Scholes).
        
        S: Spot rate
        K: Strike
        r_d: Domestic interest rate
        r_f: Foreign interest rate
        sigma: Volatility
        T: Time to expiry in years
        """
        if T <= 0:
            return 0.0
            
        d1 = cls.d1(S, K, r_d, r_f, sigma, T)
        d2 = cls.d2(d1, sigma, T)
        
        if option_type == OptionType.CALL:
            price = S * np.exp(-r_f * T) * stats.norm.cdf(d1) - K * np.exp(-r_d * T) * stats.norm.cdf(d2)
        else:
            price = K * np.exp(-r_d * T) * stats.norm.cdf(-d2) - S * np.exp(-r_f * T) * stats.norm.cdf(-d1)
            
        return price
    
    @classmethod
    def delta(cls, S: float, K: float, r_d: float, r_f: float,
              sigma: float, T: float, option_type: OptionType) -> float:
        """Calculate option delta."""
        if T <= 0:
            return 0.0
            
        d1 = cls.d1(S, K, r_d, r_f, sigma, T)
        
        if option_type == OptionType.CALL:
            return np.exp(-r_f * T) * stats.norm.cdf(d1)
        else:
            return np.exp(-r_f * T) * (stats.norm.cdf(d1) - 1)
# Price an EURUSD call option
spot = 1.0850
strike = 1.0900
r_usd = 0.05    # USD rate
r_eur = 0.04    # EUR rate
volatility = 0.08  # 8% annualized vol
days_to_expiry = 30
T = days_to_expiry / 365

call_price = BlackScholes.price(spot, strike, r_usd, r_eur, volatility, T, OptionType.CALL)
put_price = BlackScholes.price(spot, strike, r_usd, r_eur, volatility, T, OptionType.PUT)
call_delta = BlackScholes.delta(spot, strike, r_usd, r_eur, volatility, T, OptionType.CALL)
put_delta = BlackScholes.delta(spot, strike, r_usd, r_eur, volatility, T, OptionType.PUT)

print(f"EURUSD Option Pricing:")
print(f"  Spot: {spot}")
print(f"  Strike: {strike}")
print(f"  Days to Expiry: {days_to_expiry}")
print(f"  Volatility: {volatility*100}%")
print(f"\nCall Option:")
print(f"  Price: {call_price:.5f} ({call_price*100000:.2f} USD per 100k lot)")
print(f"  Delta: {call_delta:.4f}")
print(f"\nPut Option:")
print(f"  Price: {put_price:.5f} ({put_price*100000:.2f} USD per 100k lot)")
print(f"  Delta: {put_delta:.4f}")
class DeltaHedger:
    """Delta hedging for option positions."""
    
    def __init__(self, option: FXOption, hedge_frequency: str = 'daily'):
        self.option = option
        self.hedge_frequency = hedge_frequency
        self.spot_position = 0.0
        self.hedge_history = []
        
    def calculate_hedge(self, spot: float, r_d: float, r_f: float, 
                       sigma: float, days_remaining: int) -> float:
        """Calculate required spot hedge."""
        T = days_remaining / 365
        delta = BlackScholes.delta(
            spot, self.option.strike, r_d, r_f, sigma, T, self.option.option_type
        )
        
        # For a long option, we need to sell delta * notional of spot
        required_hedge = -delta * self.option.notional
        return required_hedge
    
    def rebalance(self, spot: float, r_d: float, r_f: float,
                  sigma: float, days_remaining: int) -> Dict:
        """Rebalance the hedge."""
        required = self.calculate_hedge(spot, r_d, r_f, sigma, days_remaining)
        adjustment = required - self.spot_position
        self.spot_position = required
        
        trade = {
            'spot': spot,
            'required_hedge': required,
            'adjustment': adjustment,
            'days_remaining': days_remaining
        }
        self.hedge_history.append(trade)
        return trade


# Example delta hedging
option = FXOption(
    pair='EURUSD',
    option_type=OptionType.CALL,
    strike=1.0900,
    expiry_days=30,
    premium=500,
    notional=100000
)

hedger = DeltaHedger(option)

# Simulate hedging over a few days
spots = [1.0850, 1.0880, 1.0920, 1.0900]
days_left = [30, 29, 28, 27]

print("Delta Hedging Simulation:")
print("-" * 60)
for spot, days in zip(spots, days_left):
    trade = hedger.rebalance(spot, 0.05, 0.04, 0.08, days)
    print(f"Day {30-days+1}: Spot={spot:.4f}, Hedge={trade['required_hedge']:.0f}, "
          f"Adjustment={trade['adjustment']:+.0f}")

Section 12.4: Seasonal Strategies

Many markets exhibit predictable seasonal patterns.

class SeasonalAnalyzer:
    """Analyze seasonal patterns in price data."""
    
    def __init__(self, prices: pd.Series):
        self.prices = prices
        self.returns = prices.pct_change().dropna()
        
    def monthly_returns(self) -> pd.DataFrame:
        """Calculate average returns by month."""
        monthly = self.returns.groupby(self.returns.index.month).agg(['mean', 'std', 'count'])
        monthly.columns = ['avg_return', 'std', 'observations']
        monthly['t_stat'] = monthly['avg_return'] / (monthly['std'] / np.sqrt(monthly['observations']))
        monthly['win_rate'] = self.returns.groupby(self.returns.index.month).apply(lambda x: (x > 0).mean())
        return monthly
    
    def day_of_week_returns(self) -> pd.DataFrame:
        """Calculate average returns by day of week."""
        daily = self.returns.groupby(self.returns.index.dayofweek).agg(['mean', 'std', 'count'])
        daily.columns = ['avg_return', 'std', 'observations']
        daily.index = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
        return daily
    
    def turn_of_month(self, window: int = 3) -> Dict:
        """Analyze turn-of-month effect."""
        # Last N days of month
        month_end = self.returns[self.returns.index.is_month_end | 
                                  (self.returns.index + pd.Timedelta(days=1)).is_month_start]
        
        # First N days of month
        month_start = self.returns[self.returns.index.is_month_start | 
                                   (self.returns.index.day <= window)]
        
        return {
            'month_end_avg': month_end.mean(),
            'month_start_avg': month_start.mean(),
            'rest_avg': self.returns[~self.returns.index.isin(month_end.index.union(month_start.index))].mean()
        }
# Generate multi-year synthetic data with seasonal patterns
np.random.seed(42)
dates = pd.date_range('2020-01-01', '2024-12-31', freq='D')

# Base random walk
returns = np.random.randn(len(dates)) * 0.01

# Add monthly seasonality (e.g., January effect)
month_effects = {
    1: 0.002,   # January positive
    5: -0.001,  # May negative (sell in May)
    9: -0.001,  # September negative
    12: 0.002   # December positive (Santa rally)
}

for i, date in enumerate(dates):
    if date.month in month_effects:
        returns[i] += month_effects[date.month]

prices = 100 * np.exp(np.cumsum(returns))
price_series = pd.Series(prices, index=dates)

# Analyze seasonality
analyzer = SeasonalAnalyzer(price_series)
monthly = analyzer.monthly_returns()

print("Monthly Return Analysis:")
print(monthly[['avg_return', 'win_rate', 't_stat']].round(4))

# Plot monthly returns
plt.figure(figsize=(10, 5))
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
colors = ['green' if x > 0 else 'red' for x in monthly['avg_return']]
plt.bar(months, monthly['avg_return'] * 100, color=colors, alpha=0.7)
plt.title('Average Monthly Returns (%)')
plt.ylabel('Return (%)')
plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
class SeasonalStrategy:
    """Trade based on seasonal patterns."""
    
    def __init__(self, long_months: List[int], short_months: List[int] = None):
        """
        Initialize strategy.
        
        long_months: Months to go long (1-12)
        short_months: Months to go short (optional)
        """
        self.long_months = long_months
        self.short_months = short_months or []
        
    def generate_signals(self, dates: pd.DatetimeIndex) -> pd.Series:
        """Generate position signals."""
        signals = pd.Series(0, index=dates)
        
        for date in dates:
            if date.month in self.long_months:
                signals[date] = 1
            elif date.month in self.short_months:
                signals[date] = -1
                
        return signals
    
    def backtest(self, prices: pd.Series) -> pd.DataFrame:
        """Backtest the seasonal strategy."""
        returns = prices.pct_change().dropna()
        signals = self.generate_signals(returns.index)
        
        strategy_returns = returns * signals.shift(1)  # Signal from previous day
        
        results = pd.DataFrame({
            'returns': returns,
            'signal': signals,
            'strategy_returns': strategy_returns
        })
        
        results['cumulative'] = (1 + results['strategy_returns'].fillna(0)).cumprod()
        results['buy_hold'] = (1 + results['returns'].fillna(0)).cumprod()
        
        return results


# Test "Sell in May" strategy
sell_in_may = SeasonalStrategy(
    long_months=[11, 12, 1, 2, 3, 4],  # Long Nov-Apr
    short_months=[]  # Stay flat May-Oct
)

results = sell_in_may.backtest(price_series)

print("Sell in May Strategy Results:")
print(f"  Strategy Total Return: {(results['cumulative'].iloc[-1] - 1) * 100:.2f}%")
print(f"  Buy & Hold Return: {(results['buy_hold'].iloc[-1] - 1) * 100:.2f}%")
print(f"  Time in Market: {(results['signal'] != 0).mean() * 100:.1f}%")

Exercises

Exercise 1: GBP Index (Guided)

Create a GBP strength index.

class GBPStrengthIndex:
    """Calculate GBP strength against major currencies."""
    
    def __init__(self):
        # Define pairs and weights
        self.pairs = ['GBPUSD', 'GBPEUR', 'GBPJPY', 'GBPCHF', 'GBPAUD']
        self.weights = {
            'GBPUSD': 0.30,   # USD most important
            'GBPEUR': 0.25,   # EUR second
            'GBPJPY': ______,  # Fill in weight
            'GBPCHF': 0.10,
            'GBPAUD': 0.15
        }
        
    def calculate(self, rates: Dict[str, float]) -> float:
        """Calculate GBP index value."""
        index_value = 0.0
        
        for pair, weight in self.weights.items():
            rate = rates.get(pair, 1.0)
            # GBP is always first in these pairs
            # Higher rate = stronger GBP
            index_value += rate * ______  # Apply weight
            
        return index_value * 100  # Scale to 100
    
    def rate_of_change(self, current: Dict[str, float], 
                       previous: Dict[str, float]) -> float:
        """Calculate index change."""
        current_idx = self.______(current)    # Call calculate method
        previous_idx = self.calculate(previous)
        return (current_idx / previous_idx - 1) * ______  # Return as percentage


# Test the index
gbp_index = GBPStrengthIndex()
rates = {
    'GBPUSD': 1.2650,
    'GBPEUR': 1.1650,
    'GBPJPY': 189.00,
    'GBPCHF': 1.1100,
    'GBPAUD': 1.9200
}

value = gbp_index.calculate(rates)
print(f"GBP Strength Index: {value:.2f}")

Exercise 2: Spread Mean Reversion (Guided)

Build a mean reversion strategy for spreads.

class SpreadMeanReversion:
    """Mean reversion strategy for spread trading."""
    
    def __init__(self, lookback: int = 20, entry_z: float = 2.0, exit_z: float = 0.5):
        self.lookback = lookback
        self.entry_z = entry_z
        self.exit_z = exit_z
        self.position = 0  # -1, 0, or 1
        
    def calculate_z_score(self, spread: pd.Series) -> float:
        """Calculate current z-score."""
        recent = spread.iloc[-self.lookback:]
        mean = recent.______()      # Calculate mean
        std = recent.std()
        current = spread.iloc[-1]
        
        if std == 0:
            return 0.0
        return (current - mean) / ______  # Calculate z-score
    
    def generate_signal(self, spread: pd.Series) -> int:
        """Generate trading signal."""
        z = self.calculate_z_score(spread)
        
        if self.position == 0:  # No position
            if z > self.entry_z:
                self.position = ______  # Short the spread
            elif z < -self.entry_z:
                self.position = 1   # Long the spread
        else:  # Have position
            if abs(z) < self.______:  # Check exit threshold
                self.position = 0  # Exit
                
        return self.position


# Test the strategy
strategy = SpreadMeanReversion(lookback=20, entry_z=2.0, exit_z=0.5)

# Use spread_series from earlier
signals = []
for i in range(20, len(spread_series)):
    signal = strategy.generate_signal(spread_series.iloc[:i+1])
    signals.append(signal)

print(f"Signal distribution: {pd.Series(signals).value_counts().to_dict()}")

Exercise 3: Option Payoff Calculator (Guided)

Create a payoff diagram generator.

class OptionPayoffCalculator:
    """Calculate and visualize option payoffs."""
    
    def __init__(self):
        self.positions: List[Dict] = []
        
    def add_call(self, strike: float, premium: float, quantity: int = 1):
        """Add a call option position."""
        self.positions.append({
            'type': 'call',
            'strike': strike,
            'premium': premium,
            'quantity': quantity
        })
        
    def add_put(self, strike: float, premium: float, quantity: int = 1):
        """Add a put option position."""
        self.positions.append({
            'type': ______,  # Set type
            'strike': strike,
            'premium': premium,
            'quantity': quantity
        })
        
    def calculate_payoff(self, spot: float) -> float:
        """Calculate total payoff at given spot price."""
        total = 0.0
        
        for pos in self.positions:
            if pos['type'] == 'call':
                intrinsic = max(0, spot - pos['strike'])
            else:  # put
                intrinsic = max(0, pos['______'] - spot)  # Get strike
                
            payoff = (intrinsic - pos['premium']) * pos['quantity']
            total += payoff
            
        return total
    
    def plot_payoff(self, spot_range: Tuple[float, float], title: str = "Option Payoff"):
        """Plot payoff diagram."""
        spots = np.linspace(spot_range[0], spot_range[1], 100)
        payoffs = [self.calculate_payoff(s) for s in spots]
        
        plt.figure(figsize=(10, 6))
        plt.plot(spots, payoffs, 'b-', linewidth=2)
        plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
        plt.fill_between(spots, payoffs, 0, where=[p > 0 for p in payoffs], 
                        alpha=0.3, color='green', label='Profit')
        plt.fill_between(spots, payoffs, 0, where=[p < 0 for p in payoffs],
                        alpha=0.3, color='red', label='Loss')
        plt.xlabel('Spot Price')
        plt.ylabel('P&L')
        plt.title(title)
        plt.legend()
        plt.grid(True, alpha=0.3)
        plt.tight_layout()
        plt.______()  # Display the plot


# Create a bull call spread
calc = OptionPayoffCalculator()
calc.add_call(strike=1.08, premium=0.02, quantity=1)   # Buy lower strike
calc.add_call(strike=1.10, premium=0.01, quantity=-1)  # Sell higher strike

calc.plot_payoff((1.04, 1.14), "Bull Call Spread - EURUSD")

Exercise 4: Multi-Currency Basket

Create a customizable multi-currency basket index.

# Exercise 4: Build a CurrencyBasket class that:
# 1. Allows adding multiple currency pairs with custom weights
# 2. Normalizes weights to sum to 1.0
# 3. Calculates basket value from current rates
# 4. Tracks basket value over time series
# 5. Calculates correlation between individual pairs and basket

# Your code here

Exercise 5: Calendar Spread Analyzer

Build a comprehensive calendar spread analysis tool.

# Exercise 5: Build a CalendarSpreadAnalyzer class that:
# 1. Takes prices for multiple contract months
# 2. Calculates all pairwise spreads
# 3. Identifies the term structure (contango/backwardation)
# 4. Finds the most attractive spread based on historical z-score
# 5. Generates a term structure plot

# Your code here

Exercise 6: Seasonal Strategy Optimizer

Optimize a seasonal trading strategy.

# Exercise 6: Build a SeasonalOptimizer class that:
# 1. Tests all combinations of months to be long/short/flat
# 2. Evaluates strategies using Sharpe ratio
# 3. Applies out-of-sample validation
# 4. Returns the optimal month selection
# 5. Reports statistical significance of patterns

# Your code here

Module Project: Advanced Strategy Toolkit

Build a comprehensive toolkit for advanced forex/futures strategies.

class AdvancedStrategyToolkit:
    """
    Comprehensive toolkit for advanced trading strategies.
    
    Integrates:
    - Currency indices
    - Spread trading
    - Options analysis
    - Seasonal patterns
    """
    
    def __init__(self):
        self.dxy = DollarIndex()
        self.seasonal = None
        self.spreads = {}
        
    def analyze_currency_strength(self, rates: Dict[str, float]) -> Dict[str, float]:
        """
        Calculate strength for all major currencies.
        
        Returns normalized strength scores.
        """
        # Calculate DXY
        dxy_value = self.dxy.calculate(rates)
        
        # Calculate other currency indices
        eur_index = CustomCurrencyIndex('EUR', ['EURUSD', 'EURJPY', 'EURGBP'])
        gbp_index = CustomCurrencyIndex('GBP', ['GBPUSD', 'GBPJPY', 'GBPCHF'])
        jpy_pairs = ['USDJPY', 'EURJPY', 'GBPJPY']
        
        # Normalize (100 = average)
        strengths = {
            'USD': dxy_value,
            'EUR': eur_index.calculate(rates),
            'GBP': gbp_index.calculate(rates)
        }
        
        return strengths
    
    def find_spread_opportunities(self, prices: Dict[str, pd.Series], 
                                   z_threshold: float = 2.0) -> List[Dict]:
        """
        Scan for spread trading opportunities.
        
        Returns list of opportunities sorted by z-score.
        """
        opportunities = []
        
        # Define related pairs to check
        related_pairs = [
            ('EURUSD', 'GBPUSD'),
            ('AUDUSD', 'NZDUSD'),
            ('USDJPY', 'EURJPY'),
        ]
        
        for pair1, pair2 in related_pairs:
            if pair1 in prices and pair2 in prices:
                trader = CurrencyPairsTrader(pair1, pair2)
                trader.calculate_hedge_ratio(prices[pair1], prices[pair2])
                spread = trader.calculate_spread_series(prices[pair1], prices[pair2])
                
                # Calculate z-score
                mean = spread.rolling(60).mean().iloc[-1]
                std = spread.rolling(60).std().iloc[-1]
                z_score = (spread.iloc[-1] - mean) / std if std > 0 else 0
                
                if abs(z_score) > z_threshold:
                    opportunities.append({
                        'pair1': pair1,
                        'pair2': pair2,
                        'z_score': z_score,
                        'hedge_ratio': trader.hedge_ratio,
                        'correlation': trader.correlation,
                        'signal': 'SHORT_SPREAD' if z_score > 0 else 'LONG_SPREAD'
                    })
                    
        return sorted(opportunities, key=lambda x: abs(x['z_score']), reverse=True)
    
    def analyze_seasonality(self, prices: pd.Series) -> Dict:
        """
        Comprehensive seasonal analysis.
        """
        self.seasonal = SeasonalAnalyzer(prices)
        
        monthly = self.seasonal.monthly_returns()
        dow = self.seasonal.day_of_week_returns()
        tom = self.seasonal.turn_of_month()
        
        # Find significant patterns
        significant_months = monthly[abs(monthly['t_stat']) > 1.96].index.tolist()
        
        return {
            'monthly': monthly,
            'day_of_week': dow,
            'turn_of_month': tom,
            'significant_months': significant_months,
            'best_month': monthly['avg_return'].idxmax(),
            'worst_month': monthly['avg_return'].idxmin()
        }
    
    def price_option_strategy(self, spot: float, strategies: List[Dict],
                               r_d: float, r_f: float, sigma: float, T: float) -> Dict:
        """
        Price a multi-leg option strategy.
        
        strategies: List of {'type': 'call'/'put', 'strike': K, 'quantity': N}
        """
        total_premium = 0
        total_delta = 0
        legs = []
        
        for strat in strategies:
            opt_type = OptionType.CALL if strat['type'] == 'call' else OptionType.PUT
            price = BlackScholes.price(spot, strat['strike'], r_d, r_f, sigma, T, opt_type)
            delta = BlackScholes.delta(spot, strat['strike'], r_d, r_f, sigma, T, opt_type)
            
            legs.append({
                'type': strat['type'],
                'strike': strat['strike'],
                'quantity': strat['quantity'],
                'price': price,
                'delta': delta
            })
            
            total_premium += price * strat['quantity']
            total_delta += delta * strat['quantity']
            
        return {
            'legs': legs,
            'net_premium': total_premium,
            'net_delta': total_delta
        }
    
    def generate_report(self, rates: Dict, prices: Dict[str, pd.Series]) -> str:
        """Generate comprehensive market analysis report."""
        lines = ["=" * 60]
        lines.append("ADVANCED STRATEGY TOOLKIT REPORT")
        lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
        lines.append("=" * 60)
        
        # Currency strength
        lines.append("\n--- CURRENCY STRENGTH ---")
        strengths = self.analyze_currency_strength(rates)
        for ccy, strength in strengths.items():
            lines.append(f"{ccy}: {strength:.2f}")
            
        # Spread opportunities
        lines.append("\n--- SPREAD OPPORTUNITIES ---")
        opps = self.find_spread_opportunities(prices, z_threshold=1.5)
        if opps:
            for opp in opps[:3]:
                lines.append(f"{opp['pair1']}/{opp['pair2']}: Z={opp['z_score']:.2f} -> {opp['signal']}")
        else:
            lines.append("No significant opportunities found")
            
        lines.append("\n" + "=" * 60)
        return "\n".join(lines)
# Demonstrate the toolkit
toolkit = AdvancedStrategyToolkit()

# Sample rates
rates = {
    'EURUSD': 1.0850,
    'USDJPY': 149.50,
    'GBPUSD': 1.2650,
    'USDCAD': 1.3500,
    'USDSEK': 10.50,
    'USDCHF': 0.8800,
    'EURJPY': 162.20,
    'EURGBP': 0.8580,
    'GBPJPY': 189.00,
    'GBPCHF': 1.1100
}

# Generate synthetic price series for spread analysis
np.random.seed(42)
n = 100
dates = pd.date_range('2024-01-01', periods=n, freq='D')

prices = {
    'EURUSD': pd.Series(1.08 + np.cumsum(np.random.randn(n) * 0.002), index=dates),
    'GBPUSD': pd.Series(1.26 + np.cumsum(np.random.randn(n) * 0.002), index=dates),
}

# Generate report
report = toolkit.generate_report(rates, prices)
print(report)

# Analyze seasonality
seasonal = toolkit.analyze_seasonality(price_series)
print(f"\nBest Month: {seasonal['best_month']}")
print(f"Worst Month: {seasonal['worst_month']}")

Key Takeaways

  1. Currency Indices: DXY and custom indices measure relative currency strength

  2. Spread Trading: Inter-commodity, calendar, and pairs spreads offer relative value opportunities

  3. Options: FX options add strategic flexibility; delta hedging manages directional risk

  4. Seasonality: Calendar effects exist but require statistical validation

  5. Integration: Combining multiple approaches improves trading decisions


Next Module: Module 13 - Automation & Monitoring

Module 13: Automation & Monitoring

Duration ~2.5 hours
Skill Level Advanced
Prerequisites Modules 11-12

Learning Objectives

By the end of this module, you will be able to: - Build 24/7 scheduling systems with timezone awareness - Implement comprehensive alert and notification systems - Track real-time performance metrics - Monitor system health and implement failover logic

Prerequisites

  • Completed Module 11 (Live Trading)
  • Understanding of trading sessions and market hours
  • Familiarity with logging and error handling
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, time
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field
from enum import Enum
import logging
import json
from collections import deque
import threading
import time as time_module

Section 13.1: Scheduling for 24/7

Forex markets trade 24 hours, requiring sophisticated scheduling with timezone awareness.

class Timezone(Enum):
    """Major financial timezones."""
    UTC = 0
    NEW_YORK = -5  # EST (adjust for DST)
    LONDON = 0     # GMT (adjust for DST)
    TOKYO = 9
    SYDNEY = 11
    FRANKFURT = 1


class TimezoneConverter:
    """Handle timezone conversions for global markets."""
    
    # DST transitions (simplified)
    DST_REGIONS = {
        'US': {'start': (3, 2), 'end': (11, 1)},    # Mar 2nd Sun, Nov 1st Sun
        'EU': {'start': (3, 4), 'end': (10, 4)},    # Mar last Sun, Oct last Sun
        'AU': {'start': (10, 1), 'end': (4, 1)},    # Oct 1st Sun, Apr 1st Sun
    }
    
    def __init__(self, local_tz: Timezone = Timezone.UTC):
        self.local_tz = local_tz
        
    def utc_to_local(self, utc_time: datetime, target_tz: Timezone) -> datetime:
        """Convert UTC to target timezone."""
        offset = timedelta(hours=target_tz.value)
        return utc_time + offset
    
    def local_to_utc(self, local_time: datetime, source_tz: Timezone) -> datetime:
        """Convert local time to UTC."""
        offset = timedelta(hours=source_tz.value)
        return local_time - offset
    
    def get_session_times_utc(self) -> Dict[str, tuple]:
        """Get trading session times in UTC."""
        return {
            'Sydney': (21, 6),      # 21:00 - 06:00 UTC
            'Tokyo': (0, 9),        # 00:00 - 09:00 UTC
            'London': (7, 16),      # 07:00 - 16:00 UTC
            'New York': (12, 21),   # 12:00 - 21:00 UTC
        }
    
    def is_session_active(self, session: str, utc_hour: int = None) -> bool:
        """Check if a trading session is active."""
        if utc_hour is None:
            utc_hour = datetime.utcnow().hour
            
        sessions = self.get_session_times_utc()
        if session not in sessions:
            return False
            
        start, end = sessions[session]
        if start < end:
            return start <= utc_hour < end
        else:  # Crosses midnight
            return utc_hour >= start or utc_hour < end
class TradingScheduler:
    """Schedule trading activities across sessions."""
    
    def __init__(self):
        self.tz_converter = TimezoneConverter()
        self.scheduled_tasks: List[Dict] = []
        self.running = False
        
    def add_task(self, name: str, callback: Callable, 
                 schedule_type: str, **kwargs):
        """
        Add a scheduled task.
        
        schedule_type:
        - 'interval': Run every N seconds (kwargs: interval_seconds)
        - 'daily': Run at specific time (kwargs: hour, minute, timezone)
        - 'session_start': Run when session opens (kwargs: session)
        - 'session_end': Run when session closes (kwargs: session)
        """
        task = {
            'name': name,
            'callback': callback,
            'schedule_type': schedule_type,
            'last_run': None,
            'enabled': True,
            **kwargs
        }
        self.scheduled_tasks.append(task)
        
    def should_run_task(self, task: Dict, current_time: datetime) -> bool:
        """Check if task should run now."""
        if not task['enabled']:
            return False
            
        schedule_type = task['schedule_type']
        
        if schedule_type == 'interval':
            if task['last_run'] is None:
                return True
            elapsed = (current_time - task['last_run']).total_seconds()
            return elapsed >= task['interval_seconds']
            
        elif schedule_type == 'daily':
            target_hour = task['hour']
            target_minute = task.get('minute', 0)
            
            if current_time.hour == target_hour and current_time.minute == target_minute:
                # Check if already run today
                if task['last_run'] is None:
                    return True
                return task['last_run'].date() < current_time.date()
                
        elif schedule_type == 'session_start':
            session = task['session']
            sessions = self.tz_converter.get_session_times_utc()
            if session in sessions:
                start_hour, _ = sessions[session]
                if current_time.hour == start_hour and current_time.minute < 5:
                    if task['last_run'] is None:
                        return True
                    return task['last_run'].date() < current_time.date()
                    
        return False
    
    def run_pending(self) -> List[str]:
        """Run all pending tasks."""
        current_time = datetime.utcnow()
        executed = []
        
        for task in self.scheduled_tasks:
            if self.should_run_task(task, current_time):
                try:
                    task['callback']()
                    task['last_run'] = current_time
                    executed.append(task['name'])
                except Exception as e:
                    print(f"Task {task['name']} failed: {e}")
                    
        return executed
    
    def get_status(self) -> pd.DataFrame:
        """Get status of all scheduled tasks."""
        data = []
        for task in self.scheduled_tasks:
            data.append({
                'Name': task['name'],
                'Type': task['schedule_type'],
                'Enabled': task['enabled'],
                'Last Run': task['last_run']
            })
        return pd.DataFrame(data)
# Demonstrate scheduler
scheduler = TradingScheduler()

# Add various tasks
scheduler.add_task(
    name='price_check',
    callback=lambda: print("Checking prices..."),
    schedule_type='interval',
    interval_seconds=60
)

scheduler.add_task(
    name='daily_report',
    callback=lambda: print("Generating daily report..."),
    schedule_type='daily',
    hour=17,  # 5 PM UTC
    minute=0
)

scheduler.add_task(
    name='london_open',
    callback=lambda: print("London session opening..."),
    schedule_type='session_start',
    session='London'
)

print("Scheduled Tasks:")
print(scheduler.get_status())

# Run pending tasks
executed = scheduler.run_pending()
print(f"\nExecuted: {executed}")
class WeekendHandler:
    """Handle weekend market closures."""
    
    # Forex closes Friday 21:00 UTC, opens Sunday 21:00 UTC
    CLOSE_DAY = 4  # Friday
    CLOSE_HOUR = 21
    OPEN_DAY = 6   # Sunday
    OPEN_HOUR = 21
    
    def is_weekend(self, dt: datetime = None) -> bool:
        """Check if market is closed for weekend."""
        if dt is None:
            dt = datetime.utcnow()
            
        weekday = dt.weekday()
        hour = dt.hour
        
        # Saturday
        if weekday == 5:
            return True
        # Friday after close
        if weekday == self.CLOSE_DAY and hour >= self.CLOSE_HOUR:
            return True
        # Sunday before open
        if weekday == self.OPEN_DAY and hour < self.OPEN_HOUR:
            return True
            
        return False
    
    def time_to_open(self, dt: datetime = None) -> timedelta:
        """Calculate time until market opens."""
        if dt is None:
            dt = datetime.utcnow()
            
        if not self.is_weekend(dt):
            return timedelta(0)
            
        # Find next Sunday 21:00 UTC
        days_to_sunday = (6 - dt.weekday()) % 7
        if days_to_sunday == 0 and dt.hour >= self.OPEN_HOUR:
            days_to_sunday = 7
            
        next_open = dt.replace(hour=self.OPEN_HOUR, minute=0, second=0, microsecond=0)
        next_open += timedelta(days=days_to_sunday)
        
        return next_open - dt
    
    def time_to_close(self, dt: datetime = None) -> timedelta:
        """Calculate time until market closes for weekend."""
        if dt is None:
            dt = datetime.utcnow()
            
        if self.is_weekend(dt):
            return timedelta(0)
            
        # Find next Friday 21:00 UTC
        days_to_friday = (4 - dt.weekday()) % 7
        if days_to_friday == 0 and dt.hour >= self.CLOSE_HOUR:
            days_to_friday = 7
            
        next_close = dt.replace(hour=self.CLOSE_HOUR, minute=0, second=0, microsecond=0)
        next_close += timedelta(days=days_to_friday)
        
        return next_close - dt


# Test weekend handler
weekend = WeekendHandler()

# Test different times
test_times = [
    datetime(2024, 1, 12, 20, 0),  # Friday 20:00 - open
    datetime(2024, 1, 12, 22, 0),  # Friday 22:00 - closed
    datetime(2024, 1, 13, 12, 0),  # Saturday - closed
    datetime(2024, 1, 14, 20, 0),  # Sunday 20:00 - closed
    datetime(2024, 1, 14, 22, 0),  # Sunday 22:00 - open
]

print("Weekend Status Check:")
for dt in test_times:
    is_closed = weekend.is_weekend(dt)
    print(f"{dt.strftime('%A %H:%M')}: {'CLOSED' if is_closed else 'OPEN'}")

Section 13.2: Alerts & Notifications

Comprehensive alerting for price movements, signals, and system events.

class AlertLevel(Enum):
    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"


@dataclass
class Alert:
    """Represents a single alert."""
    id: str
    level: AlertLevel
    category: str
    message: str
    timestamp: datetime
    data: Dict = field(default_factory=dict)
    acknowledged: bool = False
    
    def to_dict(self) -> Dict:
        return {
            'id': self.id,
            'level': self.level.value,
            'category': self.category,
            'message': self.message,
            'timestamp': self.timestamp.isoformat(),
            'data': self.data
        }


class AlertManager:
    """Manage alerts and notifications."""
    
    def __init__(self, max_alerts: int = 1000):
        self.alerts: deque = deque(maxlen=max_alerts)
        self.alert_count = 0
        self.handlers: List[Callable] = []
        self.suppression_rules: Dict[str, datetime] = {}
        
    def add_handler(self, handler: Callable):
        """Add a notification handler (email, SMS, etc.)."""
        self.handlers.append(handler)
        
    def suppress(self, category: str, duration_minutes: int):
        """Suppress alerts of a category for a duration."""
        self.suppression_rules[category] = datetime.now() + timedelta(minutes=duration_minutes)
        
    def _is_suppressed(self, category: str) -> bool:
        """Check if category is suppressed."""
        if category in self.suppression_rules:
            if datetime.now() < self.suppression_rules[category]:
                return True
            else:
                del self.suppression_rules[category]
        return False
        
    def create_alert(self, level: AlertLevel, category: str, 
                    message: str, data: Dict = None) -> Optional[Alert]:
        """Create and dispatch an alert."""
        if self._is_suppressed(category):
            return None
            
        self.alert_count += 1
        alert = Alert(
            id=f"ALT-{self.alert_count:06d}",
            level=level,
            category=category,
            message=message,
            timestamp=datetime.now(),
            data=data or {}
        )
        
        self.alerts.append(alert)
        
        # Dispatch to handlers
        for handler in self.handlers:
            try:
                handler(alert)
            except Exception as e:
                print(f"Handler error: {e}")
                
        return alert
    
    def get_active_alerts(self, level: AlertLevel = None) -> List[Alert]:
        """Get unacknowledged alerts."""
        alerts = [a for a in self.alerts if not a.acknowledged]
        if level:
            alerts = [a for a in alerts if a.level == level]
        return alerts
    
    def acknowledge(self, alert_id: str):
        """Acknowledge an alert."""
        for alert in self.alerts:
            if alert.id == alert_id:
                alert.acknowledged = True
                break
    
    def get_summary(self) -> Dict:
        """Get alert summary."""
        active = self.get_active_alerts()
        return {
            'total': len(self.alerts),
            'active': len(active),
            'critical': len([a for a in active if a.level == AlertLevel.CRITICAL]),
            'warning': len([a for a in active if a.level == AlertLevel.WARNING]),
            'info': len([a for a in active if a.level == AlertLevel.INFO])
        }
class PriceAlertMonitor:
    """Monitor prices and generate alerts."""
    
    def __init__(self, alert_manager: AlertManager):
        self.alert_manager = alert_manager
        self.price_alerts: List[Dict] = []
        
    def add_price_alert(self, instrument: str, condition: str, 
                        price: float, message: str = None):
        """
        Add a price alert.
        
        condition: 'above', 'below', 'cross_above', 'cross_below'
        """
        alert = {
            'instrument': instrument,
            'condition': condition,
            'price': price,
            'message': message or f"{instrument} {condition} {price}",
            'triggered': False,
            'last_price': None
        }
        self.price_alerts.append(alert)
        
    def check_prices(self, current_prices: Dict[str, float]):
        """Check all price alerts against current prices."""
        for alert in self.price_alerts:
            if alert['triggered']:
                continue
                
            instrument = alert['instrument']
            if instrument not in current_prices:
                continue
                
            current = current_prices[instrument]
            target = alert['price']
            condition = alert['condition']
            last = alert['last_price']
            
            should_trigger = False
            
            if condition == 'above' and current > target:
                should_trigger = True
            elif condition == 'below' and current < target:
                should_trigger = True
            elif condition == 'cross_above' and last is not None:
                if last <= target and current > target:
                    should_trigger = True
            elif condition == 'cross_below' and last is not None:
                if last >= target and current < target:
                    should_trigger = True
                    
            if should_trigger:
                alert['triggered'] = True
                self.alert_manager.create_alert(
                    level=AlertLevel.INFO,
                    category='price_alert',
                    message=alert['message'],
                    data={'instrument': instrument, 'price': current, 'target': target}
                )
                
            alert['last_price'] = current
class SignalAlertMonitor:
    """Monitor trading signals and generate alerts."""
    
    def __init__(self, alert_manager: AlertManager):
        self.alert_manager = alert_manager
        
    def on_signal(self, signal: Dict):
        """Handle a new trading signal."""
        instrument = signal.get('instrument', 'Unknown')
        direction = signal.get('direction', 'Unknown')
        strength = signal.get('strength', 0)
        
        # Determine alert level based on signal strength
        if strength > 0.8:
            level = AlertLevel.CRITICAL
        elif strength > 0.5:
            level = AlertLevel.WARNING
        else:
            level = AlertLevel.INFO
            
        self.alert_manager.create_alert(
            level=level,
            category='trading_signal',
            message=f"{direction.upper()} signal on {instrument} (strength: {strength:.2f})",
            data=signal
        )


class ErrorAlertMonitor:
    """Monitor system errors and generate alerts."""
    
    def __init__(self, alert_manager: AlertManager):
        self.alert_manager = alert_manager
        self.error_counts: Dict[str, int] = {}
        self.threshold = 3  # Alert after 3 errors of same type
        
    def on_error(self, error_type: str, error_message: str, context: Dict = None):
        """Handle an error."""
        # Track error count
        self.error_counts[error_type] = self.error_counts.get(error_type, 0) + 1
        count = self.error_counts[error_type]
        
        # Determine level based on frequency
        if count >= self.threshold * 3:
            level = AlertLevel.CRITICAL
        elif count >= self.threshold:
            level = AlertLevel.WARNING
        else:
            level = AlertLevel.INFO
            
        self.alert_manager.create_alert(
            level=level,
            category='system_error',
            message=f"{error_type}: {error_message} (count: {count})",
            data={'error_type': error_type, 'count': count, 'context': context or {}}
        )
# Demonstrate alert system
alert_manager = AlertManager()

# Add console handler
def console_handler(alert: Alert):
    level_icon = {'info': 'i', 'warning': '!', 'critical': 'X'}[alert.level.value]
    print(f"[{level_icon}] {alert.timestamp.strftime('%H:%M:%S')} - {alert.message}")

alert_manager.add_handler(console_handler)

# Setup monitors
price_monitor = PriceAlertMonitor(alert_manager)
signal_monitor = SignalAlertMonitor(alert_manager)
error_monitor = ErrorAlertMonitor(alert_manager)

# Add price alerts
price_monitor.add_price_alert('EURUSD', 'above', 1.0900, 'EURUSD broke above 1.0900!')
price_monitor.add_price_alert('GBPUSD', 'below', 1.2600, 'GBPUSD fell below 1.2600!')

# Simulate price updates
print("\n--- Price Updates ---")
price_monitor.check_prices({'EURUSD': 1.0850, 'GBPUSD': 1.2650})
price_monitor.check_prices({'EURUSD': 1.0920, 'GBPUSD': 1.2580})

# Simulate signal
print("\n--- Trading Signal ---")
signal_monitor.on_signal({
    'instrument': 'EURUSD',
    'direction': 'buy',
    'strength': 0.85,
    'entry': 1.0920
})

# Summary
print(f"\nAlert Summary: {alert_manager.get_summary()}")

Section 13.3: Performance Tracking

Real-time performance monitoring and reporting.

@dataclass
class TradeRecord:
    """Record of a completed trade."""
    trade_id: str
    instrument: str
    direction: str
    entry_price: float
    exit_price: float
    units: float
    entry_time: datetime
    exit_time: datetime
    pnl: float
    pnl_pips: float
    
    @property
    def duration(self) -> timedelta:
        return self.exit_time - self.entry_time
    
    @property
    def is_winner(self) -> bool:
        return self.pnl > 0


class PerformanceTracker:
    """Track and analyze trading performance."""
    
    def __init__(self, initial_balance: float):
        self.initial_balance = initial_balance
        self.current_balance = initial_balance
        self.trades: List[TradeRecord] = []
        self.equity_curve: List[Dict] = [{
            'timestamp': datetime.now(),
            'balance': initial_balance,
            'equity': initial_balance
        }]
        self.daily_pnl: Dict[str, float] = {}
        
    def record_trade(self, trade: TradeRecord):
        """Record a completed trade."""
        self.trades.append(trade)
        self.current_balance += trade.pnl
        
        # Update equity curve
        self.equity_curve.append({
            'timestamp': trade.exit_time,
            'balance': self.current_balance,
            'equity': self.current_balance
        })
        
        # Update daily P&L
        date_key = trade.exit_time.strftime('%Y-%m-%d')
        self.daily_pnl[date_key] = self.daily_pnl.get(date_key, 0) + trade.pnl
        
    def get_statistics(self) -> Dict:
        """Calculate comprehensive statistics."""
        if not self.trades:
            return {'trades': 0}
            
        winners = [t for t in self.trades if t.is_winner]
        losers = [t for t in self.trades if not t.is_winner]
        
        total_pnl = sum(t.pnl for t in self.trades)
        
        avg_win = np.mean([t.pnl for t in winners]) if winners else 0
        avg_loss = np.mean([t.pnl for t in losers]) if losers else 0
        
        # Calculate max drawdown
        equity = [e['equity'] for e in self.equity_curve]
        peak = equity[0]
        max_dd = 0
        for e in equity:
            if e > peak:
                peak = e
            dd = (peak - e) / peak
            max_dd = max(max_dd, dd)
            
        # Calculate profit factor
        gross_profit = sum(t.pnl for t in winners)
        gross_loss = abs(sum(t.pnl for t in losers))
        profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
        
        return {
            'total_trades': len(self.trades),
            'winners': len(winners),
            'losers': len(losers),
            'win_rate': len(winners) / len(self.trades) * 100,
            'total_pnl': total_pnl,
            'return_pct': (self.current_balance / self.initial_balance - 1) * 100,
            'avg_win': avg_win,
            'avg_loss': avg_loss,
            'profit_factor': profit_factor,
            'max_drawdown_pct': max_dd * 100,
            'avg_trade_duration': np.mean([t.duration.total_seconds() / 3600 for t in self.trades])
        }
class RealTimePnLTracker:
    """Track P&L in real-time."""
    
    def __init__(self, alert_manager: AlertManager = None):
        self.positions: Dict[str, Dict] = {}
        self.realized_pnl = 0.0
        self.alert_manager = alert_manager
        self.daily_limit = -1000  # Max daily loss
        
    def open_position(self, instrument: str, units: float, 
                      entry_price: float, pip_value: float = 10.0):
        """Record position opening."""
        self.positions[instrument] = {
            'units': units,
            'entry_price': entry_price,
            'pip_value': pip_value,
            'current_price': entry_price,
            'unrealized_pnl': 0.0
        }
        
    def update_price(self, instrument: str, current_price: float):
        """Update price and recalculate unrealized P&L."""
        if instrument not in self.positions:
            return
            
        pos = self.positions[instrument]
        pos['current_price'] = current_price
        
        # Calculate unrealized P&L
        price_diff = current_price - pos['entry_price']
        if pos['units'] < 0:  # Short position
            price_diff = -price_diff
            
        pips = price_diff * 10000  # Assuming 4 decimal places
        pos['unrealized_pnl'] = pips * pos['pip_value'] * abs(pos['units']) / 100000
        
    def close_position(self, instrument: str) -> float:
        """Close position and realize P&L."""
        if instrument not in self.positions:
            return 0.0
            
        pnl = self.positions[instrument]['unrealized_pnl']
        self.realized_pnl += pnl
        del self.positions[instrument]
        
        # Check daily limit
        self._check_daily_limit()
        
        return pnl
    
    def _check_daily_limit(self):
        """Check if daily loss limit is breached."""
        total_pnl = self.get_total_pnl()
        if total_pnl < self.daily_limit and self.alert_manager:
            self.alert_manager.create_alert(
                level=AlertLevel.CRITICAL,
                category='risk_limit',
                message=f"Daily loss limit breached: ${total_pnl:.2f}",
                data={'realized': self.realized_pnl, 'unrealized': self.get_unrealized_pnl()}
            )
    
    def get_unrealized_pnl(self) -> float:
        """Get total unrealized P&L."""
        return sum(p['unrealized_pnl'] for p in self.positions.values())
    
    def get_total_pnl(self) -> float:
        """Get total P&L (realized + unrealized)."""
        return self.realized_pnl + self.get_unrealized_pnl()
    
    def get_position_summary(self) -> pd.DataFrame:
        """Get summary of all positions."""
        if not self.positions:
            return pd.DataFrame()
            
        data = []
        for inst, pos in self.positions.items():
            data.append({
                'Instrument': inst,
                'Units': pos['units'],
                'Entry': pos['entry_price'],
                'Current': pos['current_price'],
                'Unrealized P&L': pos['unrealized_pnl']
            })
        return pd.DataFrame(data)
# Demonstrate performance tracking
tracker = PerformanceTracker(initial_balance=10000)

# Simulate some trades
sample_trades = [
    TradeRecord('T001', 'EURUSD', 'long', 1.0800, 1.0850, 10000, 
                datetime(2024,1,1,10,0), datetime(2024,1,1,14,0), 50, 50),
    TradeRecord('T002', 'GBPUSD', 'short', 1.2700, 1.2650, 10000,
                datetime(2024,1,2,9,0), datetime(2024,1,2,16,0), 50, 50),
    TradeRecord('T003', 'USDJPY', 'long', 148.00, 147.50, 10000,
                datetime(2024,1,3,8,0), datetime(2024,1,3,12,0), -33, -50),
    TradeRecord('T004', 'EURUSD', 'long', 1.0850, 1.0920, 10000,
                datetime(2024,1,4,10,0), datetime(2024,1,5,10,0), 70, 70),
]

for trade in sample_trades:
    tracker.record_trade(trade)

# Get statistics
stats = tracker.get_statistics()
print("Performance Statistics:")
print("-" * 40)
for key, value in stats.items():
    if isinstance(value, float):
        print(f"{key}: {value:.2f}")
    else:
        print(f"{key}: {value}")
class DailySummaryReport:
    """Generate daily performance summaries."""
    
    def __init__(self, tracker: PerformanceTracker):
        self.tracker = tracker
        
    def generate(self, date: str = None) -> str:
        """Generate daily summary report."""
        if date is None:
            date = datetime.now().strftime('%Y-%m-%d')
            
        # Get trades for the date
        day_trades = [t for t in self.tracker.trades 
                     if t.exit_time.strftime('%Y-%m-%d') == date]
        
        if not day_trades:
            return f"No trades on {date}"
            
        day_pnl = sum(t.pnl for t in day_trades)
        winners = [t for t in day_trades if t.is_winner]
        
        lines = [
            f"\n{'='*50}",
            f"DAILY PERFORMANCE SUMMARY - {date}",
            f"{'='*50}",
            f"",
            f"Trades: {len(day_trades)}",
            f"Winners: {len(winners)} ({len(winners)/len(day_trades)*100:.1f}%)",
            f"Losers: {len(day_trades) - len(winners)}",
            f"",
            f"Daily P&L: ${day_pnl:,.2f}",
            f"Account Balance: ${self.tracker.current_balance:,.2f}",
            f"",
            f"Trade Details:",
            f"{'-'*50}"
        ]
        
        for t in day_trades:
            status = 'WIN' if t.is_winner else 'LOSS'
            lines.append(f"  {t.instrument} {t.direction}: {t.pnl_pips:+.1f} pips (${t.pnl:+.2f}) [{status}]")
            
        lines.append(f"{'='*50}")
        return "\n".join(lines)


# Generate daily report
report = DailySummaryReport(tracker)
print(report.generate('2024-01-01'))

Section 13.4: System Health

Monitor system health and implement failover logic.

class HealthStatus(Enum):
    HEALTHY = "healthy"
    DEGRADED = "degraded"
    CRITICAL = "critical"
    OFFLINE = "offline"


@dataclass
class HealthCheck:
    """Result of a health check."""
    name: str
    status: HealthStatus
    message: str
    latency_ms: float = 0.0
    last_check: datetime = field(default_factory=datetime.now)


class SystemHealthMonitor:
    """Comprehensive system health monitoring."""
    
    def __init__(self, alert_manager: AlertManager = None):
        self.alert_manager = alert_manager
        self.checks: Dict[str, HealthCheck] = {}
        self.check_history: Dict[str, List[HealthCheck]] = {}
        self.max_history = 100
        
    def register_check(self, name: str, checker: Callable[[], HealthCheck]):
        """Register a health check function."""
        self.checks[name] = None
        self.check_history[name] = []
        self._checkers = getattr(self, '_checkers', {})
        self._checkers[name] = checker
        
    def run_check(self, name: str) -> HealthCheck:
        """Run a specific health check."""
        if name not in self._checkers:
            return HealthCheck(name, HealthStatus.OFFLINE, "Check not registered")
            
        start = time_module.time()
        try:
            result = self._checkers[name]()
            result.latency_ms = (time_module.time() - start) * 1000
        except Exception as e:
            result = HealthCheck(name, HealthStatus.CRITICAL, str(e))
            result.latency_ms = (time_module.time() - start) * 1000
            
        # Store result
        self.checks[name] = result
        self.check_history[name].append(result)
        if len(self.check_history[name]) > self.max_history:
            self.check_history[name].pop(0)
            
        # Alert on status changes
        self._check_status_change(name, result)
        
        return result
    
    def run_all_checks(self) -> Dict[str, HealthCheck]:
        """Run all registered health checks."""
        for name in self._checkers:
            self.run_check(name)
        return self.checks
    
    def _check_status_change(self, name: str, current: HealthCheck):
        """Check for status changes and alert."""
        history = self.check_history[name]
        if len(history) < 2:
            return
            
        previous = history[-2]
        if previous.status != current.status and self.alert_manager:
            level = AlertLevel.CRITICAL if current.status in [HealthStatus.CRITICAL, HealthStatus.OFFLINE] else AlertLevel.WARNING
            self.alert_manager.create_alert(
                level=level,
                category='health_status',
                message=f"{name} status changed: {previous.status.value} -> {current.status.value}",
                data={'check': name, 'previous': previous.status.value, 'current': current.status.value}
            )
    
    def get_overall_status(self) -> HealthStatus:
        """Get overall system health status."""
        if not self.checks:
            return HealthStatus.OFFLINE
            
        statuses = [c.status for c in self.checks.values() if c is not None]
        
        if HealthStatus.OFFLINE in statuses or HealthStatus.CRITICAL in statuses:
            return HealthStatus.CRITICAL
        elif HealthStatus.DEGRADED in statuses:
            return HealthStatus.DEGRADED
        else:
            return HealthStatus.HEALTHY
    
    def get_uptime(self, name: str) -> float:
        """Calculate uptime percentage for a check."""
        history = self.check_history.get(name, [])
        if not history:
            return 0.0
            
        healthy = len([h for h in history if h.status == HealthStatus.HEALTHY])
        return healthy / len(history) * 100
    
    def generate_report(self) -> str:
        """Generate health status report."""
        lines = [
            f"\n{'='*60}",
            f"SYSTEM HEALTH REPORT - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            f"{'='*60}",
            f"",
            f"Overall Status: {self.get_overall_status().value.upper()}",
            f"",
            f"Component Status:",
            f"{'-'*60}"
        ]
        
        status_icons = {
            HealthStatus.HEALTHY: '[OK]',
            HealthStatus.DEGRADED: '[!!]',
            HealthStatus.CRITICAL: '[XX]',
            HealthStatus.OFFLINE: '[--]'
        }
        
        for name, check in self.checks.items():
            if check:
                icon = status_icons[check.status]
                uptime = self.get_uptime(name)
                lines.append(f"  {icon} {name}: {check.message} (latency: {check.latency_ms:.1f}ms, uptime: {uptime:.1f}%)")
            else:
                lines.append(f"  [--] {name}: Not checked")
                
        lines.append(f"{'='*60}")
        return "\n".join(lines)
class DataQualityChecker:
    """Check data feed quality."""
    
    def __init__(self):
        self.last_prices: Dict[str, Dict] = {}
        self.gap_threshold = 0.01  # 1% price gap is suspicious
        self.stale_threshold = 60  # Seconds before data is considered stale
        
    def check_price(self, instrument: str, price: float, timestamp: datetime) -> Dict:
        """Check price data quality."""
        issues = []
        
        if instrument in self.last_prices:
            last = self.last_prices[instrument]
            
            # Check for gaps
            change = abs(price / last['price'] - 1)
            if change > self.gap_threshold:
                issues.append(f"Large price gap: {change*100:.2f}%")
                
            # Check for stale data
            time_diff = (timestamp - last['timestamp']).total_seconds()
            if time_diff > self.stale_threshold:
                issues.append(f"Data was stale for {time_diff:.0f}s")
                
        # Update last price
        self.last_prices[instrument] = {
            'price': price,
            'timestamp': timestamp
        }
        
        return {
            'instrument': instrument,
            'price': price,
            'issues': issues,
            'is_valid': len(issues) == 0
        }


class FailoverManager:
    """Manage failover between primary and backup systems."""
    
    def __init__(self):
        self.primary_active = True
        self.failover_count = 0
        self.last_failover: datetime = None
        self.cooldown_minutes = 5
        
    def trigger_failover(self, reason: str) -> bool:
        """Trigger failover to backup system."""
        # Check cooldown
        if self.last_failover:
            elapsed = (datetime.now() - self.last_failover).total_seconds() / 60
            if elapsed < self.cooldown_minutes:
                return False
                
        self.primary_active = not self.primary_active
        self.failover_count += 1
        self.last_failover = datetime.now()
        
        system = "backup" if not self.primary_active else "primary"
        print(f"FAILOVER: Switched to {system} system. Reason: {reason}")
        
        return True
    
    def get_active_system(self) -> str:
        """Get currently active system."""
        return "primary" if self.primary_active else "backup"
# Demonstrate health monitoring
health_monitor = SystemHealthMonitor(alert_manager)

# Register health checks
def check_broker_connection():
    # Simulated check
    return HealthCheck("broker_connection", HealthStatus.HEALTHY, "Connected to broker")

def check_data_feed():
    return HealthCheck("data_feed", HealthStatus.HEALTHY, "Data feed active")

def check_strategy_engine():
    return HealthCheck("strategy_engine", HealthStatus.HEALTHY, "Strategy running")

def check_database():
    # Simulate degraded state
    return HealthCheck("database", HealthStatus.DEGRADED, "High latency detected")

health_monitor.register_check("broker_connection", check_broker_connection)
health_monitor.register_check("data_feed", check_data_feed)
health_monitor.register_check("strategy_engine", check_strategy_engine)
health_monitor.register_check("database", check_database)

# Run all checks
health_monitor.run_all_checks()

# Generate report
print(health_monitor.generate_report())

Exercises

Exercise 1: Session-Based Scheduler (Guided)

Create a scheduler that runs different logic for different trading sessions.

class SessionBasedScheduler:
    """Run different tasks based on trading session."""
    
    def __init__(self):
        self.session_tasks: Dict[str, List[Callable]] = {
            'Sydney': [],
            'Tokyo': [],
            'London': [],
            'New_York': []
        }
        self.tz_converter = TimezoneConverter()
        
    def add_session_task(self, session: str, task: Callable):
        """Add a task for a specific session."""
        if session in self.session_tasks:
            self.session_tasks[session].______(task)  # Add task to list
            
    def get_current_sessions(self) -> List[str]:
        """Get currently active sessions."""
        current_hour = datetime.utcnow().hour
        active = []
        
        for session in ['Sydney', 'Tokyo', 'London', 'New_York']:
            if self.tz_converter.is_session_active(session.replace('_', ' '), current_hour):
                active.append(______) # Append session name
                
        return active
    
    def run_session_tasks(self) -> Dict[str, int]:
        """Run all tasks for currently active sessions."""
        results = {}
        active_sessions = self.get_current_sessions()
        
        for session in active_sessions:
            tasks = self.session_tasks.get(session, [])
            executed = 0
            
            for task in ______:  # Iterate over tasks
                try:
                    task()
                    executed += 1
                except Exception as e:
                    print(f"Task error in {session}: {e}")
                    
            results[session] = executed
            
        return results


# Test the scheduler
session_scheduler = SessionBasedScheduler()

session_scheduler.add_session_task('London', lambda: print("  Checking EUR pairs..."))
session_scheduler.add_session_task('New_York', lambda: print("  Checking USD pairs..."))

print(f"Active sessions: {session_scheduler.get_current_sessions()}")
print("Running session tasks:")
results = session_scheduler.run_session_tasks()
print(f"Results: {results}")

Exercise 2: Multi-Channel Alert System (Guided)

Create an alert system with multiple notification channels.

class NotificationChannel:
    """Base class for notification channels."""
    
    def send(self, alert: Alert) -> bool:
        raise NotImplementedError


class ConsoleChannel(NotificationChannel):
    """Print alerts to console."""
    
    def send(self, alert: Alert) -> bool:
        print(f"[{alert.level.value.upper()}] {alert.message}")
        return True


class EmailChannel(NotificationChannel):
    """Send alerts via email (simulated)."""
    
    def __init__(self, recipient: str):
        self.recipient = recipient
        
    def send(self, alert: Alert) -> bool:
        print(f"EMAIL to {self.recipient}: {alert.message}")
        return ______  # Return success status


class MultiChannelAlertManager:
    """Alert manager with multiple notification channels."""
    
    def __init__(self):
        self.channels: Dict[str, NotificationChannel] = {}
        self.routing_rules: Dict[AlertLevel, List[str]] = {
            AlertLevel.INFO: [],
            AlertLevel.WARNING: [],
            AlertLevel.CRITICAL: []
        }
        self.alert_count = 0
        
    def add_channel(self, name: str, channel: NotificationChannel):
        """Add a notification channel."""
        self.channels[______] = channel  # Store channel by name
        
    def set_routing(self, level: AlertLevel, channels: List[str]):
        """Set which channels receive which alert levels."""
        self.routing_rules[level] = channels
        
    def send_alert(self, level: AlertLevel, message: str, data: Dict = None):
        """Create and send alert through appropriate channels."""
        self.alert_count += 1
        
        alert = Alert(
            id=f"ALT-{self.alert_count:06d}",
            level=level,
            category='general',
            message=message,
            timestamp=datetime.now(),
            data=data or {}
        )
        
        # Get channels for this level
        channel_names = self.routing_rules.get(level, [])
        
        for name in channel_names:
            if name in self.______:  # Access channels dict
                self.channels[name].send(alert)


# Test multi-channel alerts
mcam = MultiChannelAlertManager()

mcam.add_channel('console', ConsoleChannel())
mcam.add_channel('email', EmailChannel('trader@example.com'))

# Route INFO to console only, WARNING and CRITICAL to both
mcam.set_routing(AlertLevel.INFO, ['console'])
mcam.set_routing(AlertLevel.WARNING, ['console', 'email'])
mcam.set_routing(AlertLevel.CRITICAL, ['console', 'email'])

print("\nSending alerts:")
mcam.send_alert(AlertLevel.INFO, "System started")
mcam.send_alert(AlertLevel.CRITICAL, "Connection lost!")

Exercise 3: Performance Dashboard (Guided)

Create a real-time performance dashboard data structure.

class PerformanceDashboard:
    """Real-time performance dashboard."""
    
    def __init__(self, tracker: PerformanceTracker):
        self.tracker = tracker
        self.refresh_interval = 60  # seconds
        self.last_refresh = None
        
    def get_dashboard_data(self) -> Dict:
        """Get all dashboard data."""
        stats = self.tracker.get_statistics()
        
        return {
            'summary': self._get_summary(stats),
            'recent_trades': self._get_recent_trades(5),
            'daily_pnl': self._get_daily_pnl(),
            'updated_at': datetime.now().isoformat()
        }
    
    def _get_summary(self, stats: Dict) -> Dict:
        """Get summary metrics."""
        return {
            'total_pnl': stats.get('total_pnl', 0),
            'win_rate': stats.get('______', 0),  # Get win rate
            'total_trades': stats.get('total_trades', 0),
            'profit_factor': stats.get('profit_factor', 0)
        }
    
    def _get_recent_trades(self, n: int) -> List[Dict]:
        """Get N most recent trades."""
        trades = self.tracker.trades[-n:] if self.tracker.trades else []
        return [
            {
                'instrument': t.instrument,
                'direction': t.direction,
                'pnl': t.______,  # Get P&L
                'time': t.exit_time.isoformat()
            }
            for t in reversed(trades)
        ]
    
    def _get_daily_pnl(self) -> List[Dict]:
        """Get daily P&L for charting."""
        return [
            {'date': date, 'pnl': pnl}
            for date, pnl in self.tracker.daily_pnl.______()
        ]  # Iterate over items


# Create dashboard
dashboard = PerformanceDashboard(tracker)
data = dashboard.get_dashboard_data()

print("\nDashboard Data:")
print(json.dumps(data, indent=2, default=str))

Exercise 4: 24/7 Task Runner

Build a complete 24/7 task runner with weekend handling.

# Exercise 4: Build a TwentyFourSevenRunner class that:
# 1. Runs continuously with configurable sleep interval
# 2. Automatically pauses during weekends
# 3. Executes different tasks based on trading session
# 4. Logs all activity
# 5. Handles graceful shutdown

# Your code here

Exercise 5: Advanced Alert Rules

Create an advanced alert rule engine.

# Exercise 5: Build an AlertRuleEngine class that:
# 1. Supports complex conditions (AND, OR, NOT)
# 2. Has rate limiting per rule (max N alerts per hour)
# 3. Supports time-based rules (only alert during certain hours)
# 4. Can escalate alerts if condition persists
# 5. Generates alert statistics

# Your code here

Exercise 6: System Recovery

Build an automatic system recovery manager.

# Exercise 6: Build a SystemRecoveryManager class that:
# 1. Detects when components are unhealthy
# 2. Attempts automatic recovery (reconnect, restart)
# 3. Implements exponential backoff for retries
# 4. Triggers failover after N failed attempts
# 5. Maintains recovery audit log

# Your code here

Module Project: Production Monitoring System

Build a comprehensive production monitoring system.

class ProductionMonitoringSystem:
    """
    Complete production monitoring system.
    
    Integrates:
    - 24/7 scheduling
    - Multi-channel alerting
    - Performance tracking
    - Health monitoring
    - Failover management
    """
    
    def __init__(self, config: Dict):
        self.config = config
        
        # Initialize components
        self.alert_manager = AlertManager()
        self.scheduler = TradingScheduler()
        self.health_monitor = SystemHealthMonitor(self.alert_manager)
        self.performance_tracker = PerformanceTracker(config.get('initial_balance', 10000))
        self.pnl_tracker = RealTimePnLTracker(self.alert_manager)
        self.weekend_handler = WeekendHandler()
        self.failover = FailoverManager()
        
        # State
        self.running = False
        self.start_time = None
        
        # Setup
        self._setup_alert_handlers()
        self._setup_scheduled_tasks()
        self._setup_health_checks()
        
    def _setup_alert_handlers(self):
        """Setup notification channels."""
        def log_handler(alert: Alert):
            logging.info(f"[{alert.level.value}] {alert.message}")
        
        self.alert_manager.add_handler(log_handler)
        
    def _setup_scheduled_tasks(self):
        """Setup scheduled monitoring tasks."""
        # Health check every minute
        self.scheduler.add_task(
            name='health_check',
            callback=self._run_health_check,
            schedule_type='interval',
            interval_seconds=60
        )
        
        # Daily summary at end of NY session
        self.scheduler.add_task(
            name='daily_summary',
            callback=self._generate_daily_summary,
            schedule_type='daily',
            hour=21,
            minute=0
        )
        
    def _setup_health_checks(self):
        """Register health checks."""
        self.health_monitor.register_check(
            'system_status',
            lambda: HealthCheck('system_status', HealthStatus.HEALTHY, 'System operational')
        )
        
    def _run_health_check(self):
        """Execute health check cycle."""
        results = self.health_monitor.run_all_checks()
        overall = self.health_monitor.get_overall_status()
        
        if overall == HealthStatus.CRITICAL:
            self.failover.trigger_failover("Critical health status")
            
    def _generate_daily_summary(self):
        """Generate and send daily summary."""
        report = DailySummaryReport(self.performance_tracker)
        summary = report.generate()
        
        self.alert_manager.create_alert(
            level=AlertLevel.INFO,
            category='daily_summary',
            message="Daily Summary Generated",
            data={'summary': summary}
        )
        
    def start(self):
        """Start the monitoring system."""
        self.running = True
        self.start_time = datetime.now()
        
        self.alert_manager.create_alert(
            level=AlertLevel.INFO,
            category='system',
            message="Production monitoring system started"
        )
        
    def stop(self):
        """Stop the monitoring system."""
        self.running = False
        
        self.alert_manager.create_alert(
            level=AlertLevel.INFO,
            category='system',
            message="Production monitoring system stopped"
        )
        
    def run_cycle(self):
        """Run one monitoring cycle."""
        if not self.running:
            return
            
        # Check if weekend
        if self.weekend_handler.is_weekend():
            return
            
        # Run scheduled tasks
        executed = self.scheduler.run_pending()
        
        return executed
    
    def get_status(self) -> Dict:
        """Get comprehensive system status."""
        uptime = (datetime.now() - self.start_time).total_seconds() if self.start_time else 0
        
        return {
            'running': self.running,
            'uptime_hours': uptime / 3600,
            'health': self.health_monitor.get_overall_status().value,
            'alerts': self.alert_manager.get_summary(),
            'performance': self.performance_tracker.get_statistics(),
            'active_system': self.failover.get_active_system(),
            'is_weekend': self.weekend_handler.is_weekend()
        }
# Demonstrate the production monitoring system
config = {
    'initial_balance': 10000,
    'alert_email': 'trader@example.com'
}

monitoring_system = ProductionMonitoringSystem(config)

# Start the system
monitoring_system.start()

# Add some sample trades to the tracker
for trade in sample_trades:
    monitoring_system.performance_tracker.record_trade(trade)

# Run a few cycles
for i in range(3):
    monitoring_system.run_cycle()

# Get status
status = monitoring_system.get_status()
print("\nSystem Status:")
print(json.dumps(status, indent=2, default=str))

# Generate health report
print(monitoring_system.health_monitor.generate_report())

Key Takeaways

  1. 24/7 Scheduling: Forex requires timezone-aware scheduling with session and weekend handling

  2. Alerting: Multi-channel alerts with routing, suppression, and escalation are essential

  3. Performance Tracking: Real-time P&L and comprehensive statistics enable informed decisions

  4. Health Monitoring: Proactive monitoring prevents issues and enables automatic recovery

  5. Failover: Automatic failover between systems ensures continuous operation


Next Module: Module 14 - Trading Psychology & Journaling

Module 14: Trading Psychology & Journaling

Duration ~2.5 hours
Skill Level Advanced
Prerequisites Modules 11-13

Learning Objectives

By the end of this module, you will be able to: - Identify and mitigate common trading biases - Build a comprehensive automated trading journal - Implement systematic performance review processes - Create continuous improvement frameworks

Prerequisites

  • Completed Part 3 and Modules 12-13
  • Understanding of trading performance metrics
  • Familiarity with data analysis and visualization
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import json
from collections import defaultdict

Section 14.1: Trading Psychology

Understanding and managing psychological biases is crucial for trading success.

Common Trading Biases

Cognitive Biases: - Confirmation Bias: Seeking information that confirms existing beliefs - Recency Bias: Overweighting recent events - Anchoring: Fixating on specific price levels - Hindsight Bias: "I knew it all along"

Emotional Biases: - Loss Aversion: Pain of losses > pleasure of gains (2x typically) - Overconfidence: Overestimating prediction ability - FOMO: Fear of missing out on trades - Revenge Trading: Trading to recover losses

Leverage Psychology: - Higher leverage amplifies emotional responses - Small account moves feel larger - Can lead to premature exit or excessive risk

class TradingBias(Enum):
    """Common trading biases."""
    CONFIRMATION = "confirmation_bias"
    RECENCY = "recency_bias"
    ANCHORING = "anchoring_bias"
    LOSS_AVERSION = "loss_aversion"
    OVERCONFIDENCE = "overconfidence"
    FOMO = "fear_of_missing_out"
    REVENGE_TRADING = "revenge_trading"
    DISPOSITION_EFFECT = "disposition_effect"  # Selling winners too early, holding losers


@dataclass
class BiasIndicator:
    """Indicator for detecting a specific bias."""
    bias: TradingBias
    description: str
    detection_rule: str
    mitigation: str


class BiasDetector:
    """Detect potential trading biases from trade data."""
    
    def __init__(self):
        self.bias_indicators = self._setup_indicators()
        
    def _setup_indicators(self) -> Dict[TradingBias, BiasIndicator]:
        """Setup bias detection indicators."""
        return {
            TradingBias.LOSS_AVERSION: BiasIndicator(
                bias=TradingBias.LOSS_AVERSION,
                description="Holding losing trades too long while cutting winners short",
                detection_rule="Average losing trade duration > 2x average winning trade duration",
                mitigation="Use strict stop losses; define exit rules before entry"
            ),
            TradingBias.OVERCONFIDENCE: BiasIndicator(
                bias=TradingBias.OVERCONFIDENCE,
                description="Increasing position sizes after winning streak",
                detection_rule="Position size increases >50% after 3+ consecutive wins",
                mitigation="Use fixed position sizing rules; scale based on account, not recent results"
            ),
            TradingBias.REVENGE_TRADING: BiasIndicator(
                bias=TradingBias.REVENGE_TRADING,
                description="Increased trading frequency after losses",
                detection_rule="Trade count increases >100% in hour following losing trade",
                mitigation="Implement mandatory cooldown periods after losses"
            ),
            TradingBias.DISPOSITION_EFFECT: BiasIndicator(
                bias=TradingBias.DISPOSITION_EFFECT,
                description="Selling winners early, holding losers",
                detection_rule="Average winning trade R:R < 1.0 while holding losers beyond stop",
                mitigation="Let winners run; use trailing stops"
            ),
            TradingBias.FOMO: BiasIndicator(
                bias=TradingBias.FOMO,
                description="Entering trades after missing initial move",
                detection_rule="Entry price >80% of way to resistance/support after move started",
                mitigation="Wait for pullbacks; define entry zones before market opens"
            )
        }
    
    def analyze_trades(self, trades: List[Dict]) -> List[Dict]:
        """Analyze trades for potential biases."""
        detected_biases = []
        
        if not trades:
            return detected_biases
            
        # Check for loss aversion
        winners = [t for t in trades if t.get('pnl', 0) > 0]
        losers = [t for t in trades if t.get('pnl', 0) <= 0]
        
        if winners and losers:
            avg_win_duration = np.mean([t.get('duration_hours', 1) for t in winners])
            avg_loss_duration = np.mean([t.get('duration_hours', 1) for t in losers])
            
            if avg_loss_duration > 2 * avg_win_duration:
                detected_biases.append({
                    'bias': TradingBias.LOSS_AVERSION,
                    'severity': 'high' if avg_loss_duration > 3 * avg_win_duration else 'medium',
                    'evidence': f"Avg loss duration ({avg_loss_duration:.1f}h) > 2x avg win duration ({avg_win_duration:.1f}h)"
                })
        
        # Check for revenge trading
        for i in range(1, len(trades)):
            if trades[i-1].get('pnl', 0) < 0:
                time_diff = (trades[i].get('entry_time', datetime.now()) - 
                           trades[i-1].get('exit_time', datetime.now())).total_seconds() / 60
                if time_diff < 15:  # Trade within 15 minutes of loss
                    detected_biases.append({
                        'bias': TradingBias.REVENGE_TRADING,
                        'severity': 'medium',
                        'evidence': f"New trade within {time_diff:.0f} minutes of losing trade"
                    })
                    
        return detected_biases
    
    def get_mitigation(self, bias: TradingBias) -> str:
        """Get mitigation strategy for a bias."""
        indicator = self.bias_indicators.get(bias)
        return indicator.mitigation if indicator else "No specific mitigation available"
# Demonstrate bias detection
detector = BiasDetector()

# Sample trades with potential biases
sample_trades = [
    {'pnl': 50, 'duration_hours': 2, 'entry_time': datetime(2024,1,1,10,0), 'exit_time': datetime(2024,1,1,12,0)},
    {'pnl': -100, 'duration_hours': 8, 'entry_time': datetime(2024,1,1,13,0), 'exit_time': datetime(2024,1,1,21,0)},
    {'pnl': 30, 'duration_hours': 1, 'entry_time': datetime(2024,1,1,21,5), 'exit_time': datetime(2024,1,1,22,5)},  # Quick trade after loss
    {'pnl': -80, 'duration_hours': 10, 'entry_time': datetime(2024,1,2,9,0), 'exit_time': datetime(2024,1,2,19,0)},
]

biases = detector.analyze_trades(sample_trades)

print("Detected Biases:")
print("-" * 60)
for b in biases:
    print(f"\nBias: {b['bias'].value}")
    print(f"Severity: {b['severity']}")
    print(f"Evidence: {b['evidence']}")
    print(f"Mitigation: {detector.get_mitigation(b['bias'])}")
class EmotionalStateTracker:
    """Track emotional state during trading."""
    
    STATES = ['calm', 'focused', 'anxious', 'frustrated', 'euphoric', 'fearful']
    
    def __init__(self):
        self.entries: List[Dict] = []
        
    def log_state(self, state: str, notes: str = "", context: Dict = None):
        """Log current emotional state."""
        if state not in self.STATES:
            raise ValueError(f"Unknown state. Use one of: {self.STATES}")
            
        entry = {
            'timestamp': datetime.now(),
            'state': state,
            'notes': notes,
            'context': context or {}
        }
        self.entries.append(entry)
        
    def analyze_state_patterns(self) -> Dict:
        """Analyze patterns in emotional states."""
        if not self.entries:
            return {}
            
        state_counts = defaultdict(int)
        for entry in self.entries:
            state_counts[entry['state']] += 1
            
        total = len(self.entries)
        return {
            'distribution': {s: count/total*100 for s, count in state_counts.items()},
            'most_common': max(state_counts, key=state_counts.get),
            'total_entries': total
        }
    
    def correlate_with_performance(self, trades: List[Dict]) -> Dict:
        """Correlate emotional states with trading performance."""
        # Match trades with nearest emotional state entry
        state_performance = defaultdict(list)
        
        for trade in trades:
            trade_time = trade.get('entry_time', datetime.now())
            
            # Find nearest emotional state entry
            nearest_state = None
            min_diff = float('inf')
            
            for entry in self.entries:
                diff = abs((trade_time - entry['timestamp']).total_seconds())
                if diff < min_diff:
                    min_diff = diff
                    nearest_state = entry['state']
                    
            if nearest_state and min_diff < 3600:  # Within 1 hour
                state_performance[nearest_state].append(trade.get('pnl', 0))
                
        # Calculate average P&L by state
        return {
            state: {
                'avg_pnl': np.mean(pnls),
                'trade_count': len(pnls),
                'win_rate': len([p for p in pnls if p > 0]) / len(pnls) * 100 if pnls else 0
            }
            for state, pnls in state_performance.items()
        }


# Example usage
emotional_tracker = EmotionalStateTracker()
emotional_tracker.log_state('calm', 'Morning routine complete, ready to trade')
emotional_tracker.log_state('focused', 'Good setup developing on EURUSD')
emotional_tracker.log_state('anxious', 'Large position, uncertain about direction')
emotional_tracker.log_state('frustrated', 'Stopped out on clear manipulation')

analysis = emotional_tracker.analyze_state_patterns()
print("Emotional State Analysis:")
print(f"Most common state: {analysis.get('most_common', 'N/A')}")
print(f"Distribution: {analysis.get('distribution', {})}")

Section 14.2: Building a Trading Journal

A comprehensive trading journal is essential for improvement.

@dataclass
class JournalEntry:
    """Complete journal entry for a trade."""
    # Trade identification
    trade_id: str
    timestamp: datetime
    
    # Trade details
    instrument: str
    direction: str
    entry_price: float
    exit_price: float
    position_size: float
    
    # Risk management
    stop_loss: float
    take_profit: float
    risk_reward: float
    
    # Results
    pnl: float
    pnl_pips: float
    duration_minutes: int
    
    # Analysis
    setup_type: str
    timeframe: str
    market_conditions: str
    
    # Psychology
    emotional_state: str
    confidence_level: int  # 1-10
    followed_plan: bool
    
    # Reflection
    entry_reasoning: str = ""
    exit_reasoning: str = ""
    lessons_learned: str = ""
    what_would_do_differently: str = ""
    
    # Tags for filtering
    tags: List[str] = field(default_factory=list)
    
    # Screenshot paths
    screenshots: List[str] = field(default_factory=list)
    
    def to_dict(self) -> Dict:
        return {
            'trade_id': self.trade_id,
            'timestamp': self.timestamp.isoformat(),
            'instrument': self.instrument,
            'direction': self.direction,
            'entry_price': self.entry_price,
            'exit_price': self.exit_price,
            'position_size': self.position_size,
            'stop_loss': self.stop_loss,
            'take_profit': self.take_profit,
            'risk_reward': self.risk_reward,
            'pnl': self.pnl,
            'pnl_pips': self.pnl_pips,
            'duration_minutes': self.duration_minutes,
            'setup_type': self.setup_type,
            'timeframe': self.timeframe,
            'market_conditions': self.market_conditions,
            'emotional_state': self.emotional_state,
            'confidence_level': self.confidence_level,
            'followed_plan': self.followed_plan,
            'entry_reasoning': self.entry_reasoning,
            'exit_reasoning': self.exit_reasoning,
            'lessons_learned': self.lessons_learned,
            'what_would_do_differently': self.what_would_do_differently,
            'tags': self.tags,
            'screenshots': self.screenshots
        }
class TradingJournal:
    """Comprehensive trading journal system."""
    
    def __init__(self, journal_path: str = None):
        self.entries: List[JournalEntry] = []
        self.journal_path = journal_path
        self.entry_count = 0
        
    def add_entry(self, entry: JournalEntry):
        """Add a new journal entry."""
        self.entries.append(entry)
        self.entry_count += 1
        
    def create_entry_from_trade(self, trade: Dict, 
                                 psychological_data: Dict = None,
                                 analysis_data: Dict = None) -> JournalEntry:
        """Create a journal entry from trade data."""
        self.entry_count += 1
        
        psych = psychological_data or {}
        analysis = analysis_data or {}
        
        # Calculate risk/reward
        entry = trade.get('entry_price', 0)
        sl = trade.get('stop_loss', entry)
        tp = trade.get('take_profit', entry)
        risk = abs(entry - sl) if sl else 0
        reward = abs(tp - entry) if tp else 0
        rr = reward / risk if risk > 0 else 0
        
        return JournalEntry(
            trade_id=f"J{self.entry_count:06d}",
            timestamp=trade.get('entry_time', datetime.now()),
            instrument=trade.get('instrument', 'Unknown'),
            direction=trade.get('direction', 'Unknown'),
            entry_price=entry,
            exit_price=trade.get('exit_price', 0),
            position_size=trade.get('position_size', 0),
            stop_loss=sl,
            take_profit=tp,
            risk_reward=rr,
            pnl=trade.get('pnl', 0),
            pnl_pips=trade.get('pnl_pips', 0),
            duration_minutes=trade.get('duration_minutes', 0),
            setup_type=analysis.get('setup_type', 'Unclassified'),
            timeframe=analysis.get('timeframe', 'Unknown'),
            market_conditions=analysis.get('market_conditions', 'Unknown'),
            emotional_state=psych.get('emotional_state', 'Not recorded'),
            confidence_level=psych.get('confidence', 5),
            followed_plan=psych.get('followed_plan', True),
            entry_reasoning=analysis.get('entry_reasoning', ''),
            exit_reasoning=analysis.get('exit_reasoning', ''),
            tags=analysis.get('tags', [])
        )
    
    def search(self, **filters) -> List[JournalEntry]:
        """Search journal entries with filters."""
        results = self.entries
        
        if 'instrument' in filters:
            results = [e for e in results if e.instrument == filters['instrument']]
        if 'direction' in filters:
            results = [e for e in results if e.direction == filters['direction']]
        if 'setup_type' in filters:
            results = [e for e in results if e.setup_type == filters['setup_type']]
        if 'profitable' in filters:
            if filters['profitable']:
                results = [e for e in results if e.pnl > 0]
            else:
                results = [e for e in results if e.pnl <= 0]
        if 'tag' in filters:
            results = [e for e in results if filters['tag'] in e.tags]
        if 'date_from' in filters:
            results = [e for e in results if e.timestamp >= filters['date_from']]
        if 'date_to' in filters:
            results = [e for e in results if e.timestamp <= filters['date_to']]
            
        return results
    
    def get_statistics(self, entries: List[JournalEntry] = None) -> Dict:
        """Calculate statistics for journal entries."""
        if entries is None:
            entries = self.entries
            
        if not entries:
            return {}
            
        winners = [e for e in entries if e.pnl > 0]
        losers = [e for e in entries if e.pnl <= 0]
        
        return {
            'total_trades': len(entries),
            'win_rate': len(winners) / len(entries) * 100,
            'total_pnl': sum(e.pnl for e in entries),
            'avg_win': np.mean([e.pnl for e in winners]) if winners else 0,
            'avg_loss': np.mean([e.pnl for e in losers]) if losers else 0,
            'avg_rr_planned': np.mean([e.risk_reward for e in entries]),
            'plan_adherence': len([e for e in entries if e.followed_plan]) / len(entries) * 100,
            'avg_confidence': np.mean([e.confidence_level for e in entries]),
            'top_setup': self._get_top_setup(entries),
            'worst_setup': self._get_worst_setup(entries)
        }
    
    def _get_top_setup(self, entries: List[JournalEntry]) -> str:
        """Find best performing setup type."""
        setup_pnl = defaultdict(list)
        for e in entries:
            setup_pnl[e.setup_type].append(e.pnl)
        
        if not setup_pnl:
            return "N/A"
            
        avg_by_setup = {s: np.mean(pnls) for s, pnls in setup_pnl.items()}
        return max(avg_by_setup, key=avg_by_setup.get)
    
    def _get_worst_setup(self, entries: List[JournalEntry]) -> str:
        """Find worst performing setup type."""
        setup_pnl = defaultdict(list)
        for e in entries:
            setup_pnl[e.setup_type].append(e.pnl)
        
        if not setup_pnl:
            return "N/A"
            
        avg_by_setup = {s: np.mean(pnls) for s, pnls in setup_pnl.items()}
        return min(avg_by_setup, key=avg_by_setup.get)
    
    def save(self, path: str = None):
        """Save journal to file."""
        path = path or self.journal_path
        if not path:
            raise ValueError("No path specified")
            
        data = [e.to_dict() for e in self.entries]
        with open(path, 'w') as f:
            json.dump(data, f, indent=2)
    
    def load(self, path: str = None):
        """Load journal from file."""
        path = path or self.journal_path
        if not path:
            raise ValueError("No path specified")
            
        with open(path, 'r') as f:
            data = json.load(f)
            
        # Convert back to JournalEntry objects
        # (simplified - would need proper datetime parsing)
# Demonstrate trading journal
journal = TradingJournal()

# Add sample entries
sample_trades_for_journal = [
    {
        'instrument': 'EURUSD',
        'direction': 'long',
        'entry_price': 1.0850,
        'exit_price': 1.0920,
        'stop_loss': 1.0800,
        'take_profit': 1.0950,
        'position_size': 10000,
        'pnl': 70,
        'pnl_pips': 70,
        'duration_minutes': 180,
        'entry_time': datetime(2024, 1, 15, 10, 0)
    },
    {
        'instrument': 'GBPUSD',
        'direction': 'short',
        'entry_price': 1.2700,
        'exit_price': 1.2650,
        'stop_loss': 1.2750,
        'take_profit': 1.2600,
        'position_size': 10000,
        'pnl': 50,
        'pnl_pips': 50,
        'duration_minutes': 240,
        'entry_time': datetime(2024, 1, 16, 14, 0)
    },
    {
        'instrument': 'EURUSD',
        'direction': 'long',
        'entry_price': 1.0880,
        'exit_price': 1.0850,
        'stop_loss': 1.0850,
        'take_profit': 1.0950,
        'position_size': 10000,
        'pnl': -30,
        'pnl_pips': -30,
        'duration_minutes': 60,
        'entry_time': datetime(2024, 1, 17, 9, 0)
    }
]

for trade in sample_trades_for_journal:
    entry = journal.create_entry_from_trade(
        trade,
        psychological_data={'emotional_state': 'focused', 'confidence': 7, 'followed_plan': True},
        analysis_data={'setup_type': 'Trend Continuation', 'timeframe': 'H1', 
                      'market_conditions': 'Trending', 'tags': ['trend', 'momentum']}
    )
    journal.add_entry(entry)

# Get statistics
stats = journal.get_statistics()
print("Journal Statistics:")
print("-" * 40)
for key, value in stats.items():
    if isinstance(value, float):
        print(f"{key}: {value:.2f}")
    else:
        print(f"{key}: {value}")

Section 14.3: Performance Review

Systematic review processes for continuous improvement.

class WeeklyReview:
    """Generate weekly performance review."""
    
    def __init__(self, journal: TradingJournal):
        self.journal = journal
        
    def generate(self, week_start: datetime, week_end: datetime = None) -> Dict:
        """Generate weekly review report."""
        if week_end is None:
            week_end = week_start + timedelta(days=7)
            
        # Get trades for the week
        week_entries = self.journal.search(date_from=week_start, date_to=week_end)
        
        if not week_entries:
            return {'week': week_start.strftime('%Y-%m-%d'), 'trades': 0}
            
        stats = self.journal.get_statistics(week_entries)
        
        # Analyze by day
        daily_pnl = defaultdict(float)
        for entry in week_entries:
            day = entry.timestamp.strftime('%A')
            daily_pnl[day] += entry.pnl
            
        # Analyze by session
        session_performance = self._analyze_by_session(week_entries)
        
        # Find patterns
        patterns = self._identify_patterns(week_entries)
        
        return {
            'week': week_start.strftime('%Y-%m-%d'),
            'statistics': stats,
            'daily_pnl': dict(daily_pnl),
            'session_performance': session_performance,
            'patterns': patterns,
            'improvement_areas': self._identify_improvements(week_entries)
        }
    
    def _analyze_by_session(self, entries: List[JournalEntry]) -> Dict:
        """Analyze performance by trading session."""
        sessions = {'Asian': [], 'London': [], 'New York': []}
        
        for entry in entries:
            hour = entry.timestamp.hour
            if 0 <= hour < 8:
                sessions['Asian'].append(entry.pnl)
            elif 8 <= hour < 14:
                sessions['London'].append(entry.pnl)
            else:
                sessions['New York'].append(entry.pnl)
                
        return {
            session: {
                'trades': len(pnls),
                'total_pnl': sum(pnls),
                'avg_pnl': np.mean(pnls) if pnls else 0
            }
            for session, pnls in sessions.items()
        }
    
    def _identify_patterns(self, entries: List[JournalEntry]) -> List[str]:
        """Identify trading patterns."""
        patterns = []
        
        # Check win streaks
        max_streak = 0
        current_streak = 0
        for entry in entries:
            if entry.pnl > 0:
                current_streak += 1
                max_streak = max(max_streak, current_streak)
            else:
                current_streak = 0
                
        if max_streak >= 3:
            patterns.append(f"Winning streak of {max_streak} trades")
            
        # Check for overtrading
        trades_per_day = len(entries) / 5  # Assuming 5 trading days
        if trades_per_day > 5:
            patterns.append(f"High trading frequency: {trades_per_day:.1f} trades/day")
            
        # Check plan adherence
        adherence = len([e for e in entries if e.followed_plan]) / len(entries)
        if adherence < 0.8:
            patterns.append(f"Low plan adherence: {adherence*100:.0f}%")
            
        return patterns
    
    def _identify_improvements(self, entries: List[JournalEntry]) -> List[str]:
        """Identify areas for improvement."""
        improvements = []
        
        # Check if losing trades are too large
        losers = [e for e in entries if e.pnl < 0]
        winners = [e for e in entries if e.pnl > 0]
        
        if losers and winners:
            avg_loss = abs(np.mean([e.pnl for e in losers]))
            avg_win = np.mean([e.pnl for e in winners])
            
            if avg_loss > avg_win:
                improvements.append("Cut losers faster - avg loss > avg win")
                
        # Check emotional trading
        emotional_losses = [e for e in entries if e.pnl < 0 and e.emotional_state in ['anxious', 'frustrated', 'euphoric']]
        if emotional_losses:
            improvements.append(f"{len(emotional_losses)} losing trades during emotional states")
            
        return improvements
class MonthlyAnalysis:
    """Comprehensive monthly performance analysis."""
    
    def __init__(self, journal: TradingJournal):
        self.journal = journal
        
    def generate(self, year: int, month: int) -> Dict:
        """Generate monthly analysis report."""
        start = datetime(year, month, 1)
        if month == 12:
            end = datetime(year + 1, 1, 1)
        else:
            end = datetime(year, month + 1, 1)
            
        entries = self.journal.search(date_from=start, date_to=end)
        
        if not entries:
            return {'month': f"{year}-{month:02d}", 'trades': 0}
            
        return {
            'month': f"{year}-{month:02d}",
            'summary': self._get_summary(entries),
            'by_instrument': self._analyze_by_instrument(entries),
            'by_setup': self._analyze_by_setup(entries),
            'equity_curve': self._build_equity_curve(entries),
            'best_trade': self._get_best_trade(entries),
            'worst_trade': self._get_worst_trade(entries),
            'key_insights': self._generate_insights(entries)
        }
    
    def _get_summary(self, entries: List[JournalEntry]) -> Dict:
        """Get summary statistics."""
        return self.journal.get_statistics(entries)
    
    def _analyze_by_instrument(self, entries: List[JournalEntry]) -> Dict:
        """Analyze performance by instrument."""
        by_instrument = defaultdict(list)
        for e in entries:
            by_instrument[e.instrument].append(e)
            
        return {
            inst: {
                'trades': len(trades),
                'total_pnl': sum(t.pnl for t in trades),
                'win_rate': len([t for t in trades if t.pnl > 0]) / len(trades) * 100
            }
            for inst, trades in by_instrument.items()
        }
    
    def _analyze_by_setup(self, entries: List[JournalEntry]) -> Dict:
        """Analyze performance by setup type."""
        by_setup = defaultdict(list)
        for e in entries:
            by_setup[e.setup_type].append(e)
            
        return {
            setup: {
                'trades': len(trades),
                'total_pnl': sum(t.pnl for t in trades),
                'win_rate': len([t for t in trades if t.pnl > 0]) / len(trades) * 100,
                'avg_rr': np.mean([t.risk_reward for t in trades])
            }
            for setup, trades in by_setup.items()
        }
    
    def _build_equity_curve(self, entries: List[JournalEntry]) -> List[Dict]:
        """Build equity curve data."""
        sorted_entries = sorted(entries, key=lambda x: x.timestamp)
        equity = 0
        curve = []
        
        for e in sorted_entries:
            equity += e.pnl
            curve.append({
                'date': e.timestamp.strftime('%Y-%m-%d'),
                'equity': equity
            })
            
        return curve
    
    def _get_best_trade(self, entries: List[JournalEntry]) -> Dict:
        """Get best trade of the month."""
        best = max(entries, key=lambda x: x.pnl)
        return {
            'instrument': best.instrument,
            'pnl': best.pnl,
            'setup': best.setup_type,
            'date': best.timestamp.strftime('%Y-%m-%d')
        }
    
    def _get_worst_trade(self, entries: List[JournalEntry]) -> Dict:
        """Get worst trade of the month."""
        worst = min(entries, key=lambda x: x.pnl)
        return {
            'instrument': worst.instrument,
            'pnl': worst.pnl,
            'setup': worst.setup_type,
            'date': worst.timestamp.strftime('%Y-%m-%d')
        }
    
    def _generate_insights(self, entries: List[JournalEntry]) -> List[str]:
        """Generate actionable insights."""
        insights = []
        
        # Find best setup
        by_setup = self._analyze_by_setup(entries)
        if by_setup:
            best_setup = max(by_setup.items(), key=lambda x: x[1]['total_pnl'])
            insights.append(f"Best performing setup: {best_setup[0]} (${best_setup[1]['total_pnl']:.2f})")
            
        # Check confidence correlation
        high_conf = [e for e in entries if e.confidence_level >= 7]
        low_conf = [e for e in entries if e.confidence_level < 7]
        
        if high_conf and low_conf:
            high_conf_wr = len([e for e in high_conf if e.pnl > 0]) / len(high_conf)
            low_conf_wr = len([e for e in low_conf if e.pnl > 0]) / len(low_conf)
            
            if high_conf_wr > low_conf_wr + 0.1:
                insights.append(f"High confidence trades outperform ({high_conf_wr*100:.0f}% vs {low_conf_wr*100:.0f}% win rate)")
                
        return insights
# Generate weekly review
weekly_review = WeeklyReview(journal)
review = weekly_review.generate(datetime(2024, 1, 15))

print("Weekly Review:")
print("=" * 50)
print(f"Week of: {review['week']}")
print(f"\nStatistics:")
for key, value in review.get('statistics', {}).items():
    if isinstance(value, float):
        print(f"  {key}: {value:.2f}")
    else:
        print(f"  {key}: {value}")
        
print(f"\nPatterns: {review.get('patterns', [])}")
print(f"Improvements: {review.get('improvement_areas', [])}")

Section 14.4: Continuous Improvement

Systematic frameworks for ongoing trading improvement.

@dataclass
class ImprovementGoal:
    """A specific improvement goal."""
    id: str
    description: str
    metric: str
    target_value: float
    current_value: float
    deadline: datetime
    actions: List[str] = field(default_factory=list)
    progress_notes: List[Dict] = field(default_factory=list)
    
    @property
    def progress_pct(self) -> float:
        """Calculate progress towards goal."""
        if self.target_value == 0:
            return 0
        return (self.current_value / self.target_value) * 100
    
    @property
    def is_achieved(self) -> bool:
        return self.current_value >= self.target_value


class ImprovementTracker:
    """Track and manage improvement goals."""
    
    def __init__(self):
        self.goals: Dict[str, ImprovementGoal] = {}
        self.goal_count = 0
        
    def add_goal(self, description: str, metric: str, 
                 target_value: float, deadline: datetime,
                 current_value: float = 0, actions: List[str] = None) -> str:
        """Add a new improvement goal."""
        self.goal_count += 1
        goal_id = f"GOAL-{self.goal_count:03d}"
        
        self.goals[goal_id] = ImprovementGoal(
            id=goal_id,
            description=description,
            metric=metric,
            target_value=target_value,
            current_value=current_value,
            deadline=deadline,
            actions=actions or []
        )
        return goal_id
    
    def update_progress(self, goal_id: str, new_value: float, note: str = ""):
        """Update progress on a goal."""
        if goal_id not in self.goals:
            return
            
        goal = self.goals[goal_id]
        goal.current_value = new_value
        goal.progress_notes.append({
            'date': datetime.now(),
            'value': new_value,
            'note': note
        })
        
    def get_active_goals(self) -> List[ImprovementGoal]:
        """Get goals that are not yet achieved."""
        return [g for g in self.goals.values() if not g.is_achieved]
    
    def get_status_report(self) -> str:
        """Generate status report for all goals."""
        lines = [
            "\n" + "=" * 60,
            "IMPROVEMENT GOALS STATUS",
            "=" * 60,
            ""
        ]
        
        for goal in self.goals.values():
            status = "ACHIEVED" if goal.is_achieved else "IN PROGRESS"
            lines.append(f"[{goal.id}] {goal.description}")
            lines.append(f"  Metric: {goal.metric}")
            lines.append(f"  Progress: {goal.current_value:.2f} / {goal.target_value:.2f} ({goal.progress_pct:.0f}%)")
            lines.append(f"  Deadline: {goal.deadline.strftime('%Y-%m-%d')}")
            lines.append(f"  Status: {status}")
            lines.append("")
            
        return "\n".join(lines)
class ABTestFramework:
    """A/B testing framework for trading strategies."""
    
    def __init__(self):
        self.tests: Dict[str, Dict] = {}
        self.test_count = 0
        
    def create_test(self, name: str, description: str,
                    variant_a: str, variant_b: str,
                    sample_size: int = 50) -> str:
        """Create a new A/B test."""
        self.test_count += 1
        test_id = f"TEST-{self.test_count:03d}"
        
        self.tests[test_id] = {
            'name': name,
            'description': description,
            'variant_a': variant_a,
            'variant_b': variant_b,
            'sample_size': sample_size,
            'results_a': [],
            'results_b': [],
            'status': 'active',
            'created': datetime.now()
        }
        return test_id
    
    def record_result(self, test_id: str, variant: str, pnl: float):
        """Record a result for a test variant."""
        if test_id not in self.tests:
            return
            
        test = self.tests[test_id]
        
        if variant == 'A':
            test['results_a'].append(pnl)
        else:
            test['results_b'].append(pnl)
            
        # Check if test is complete
        if (len(test['results_a']) >= test['sample_size'] and 
            len(test['results_b']) >= test['sample_size']):
            test['status'] = 'complete'
            
    def analyze_test(self, test_id: str) -> Dict:
        """Analyze test results."""
        if test_id not in self.tests:
            return {}
            
        test = self.tests[test_id]
        results_a = test['results_a']
        results_b = test['results_b']
        
        if not results_a or not results_b:
            return {'status': 'insufficient_data'}
            
        # Calculate statistics
        mean_a = np.mean(results_a)
        mean_b = np.mean(results_b)
        
        # Simple t-test
        from scipy import stats
        t_stat, p_value = stats.ttest_ind(results_a, results_b)
        
        winner = 'A' if mean_a > mean_b else 'B'
        significant = p_value < 0.05
        
        return {
            'test_name': test['name'],
            'variant_a': {
                'description': test['variant_a'],
                'samples': len(results_a),
                'mean_pnl': mean_a,
                'win_rate': len([r for r in results_a if r > 0]) / len(results_a) * 100
            },
            'variant_b': {
                'description': test['variant_b'],
                'samples': len(results_b),
                'mean_pnl': mean_b,
                'win_rate': len([r for r in results_b if r > 0]) / len(results_b) * 100
            },
            'p_value': p_value,
            'statistically_significant': significant,
            'recommended': winner if significant else 'No clear winner'
        }
# Demonstrate improvement tracking
improvement_tracker = ImprovementTracker()

# Add improvement goals
improvement_tracker.add_goal(
    description="Improve win rate",
    metric="win_rate_pct",
    target_value=55,
    current_value=48,
    deadline=datetime(2024, 3, 1),
    actions=["Only take A+ setups", "Wait for confirmation"]
)

improvement_tracker.add_goal(
    description="Reduce average loss size",
    metric="avg_loss_usd",
    target_value=30,
    current_value=45,
    deadline=datetime(2024, 3, 1),
    actions=["Use tighter stops", "Cut losers faster"]
)

improvement_tracker.add_goal(
    description="Increase plan adherence",
    metric="plan_adherence_pct",
    target_value=90,
    current_value=75,
    deadline=datetime(2024, 2, 15),
    actions=["Pre-define entries/exits", "Use checklist"]
)

print(improvement_tracker.get_status_report())
# Demonstrate A/B testing
ab_framework = ABTestFramework()

# Create a test
test_id = ab_framework.create_test(
    name="Stop Loss Placement",
    description="Testing ATR-based stops vs fixed pip stops",
    variant_a="ATR-based stop (2x ATR)",
    variant_b="Fixed 30 pip stop",
    sample_size=20
)

# Simulate results
np.random.seed(42)

# Variant A results (ATR-based)
for _ in range(25):
    pnl = np.random.normal(15, 40)  # Slightly positive expectancy
    ab_framework.record_result(test_id, 'A', pnl)

# Variant B results (Fixed stop)
for _ in range(25):
    pnl = np.random.normal(5, 35)  # Lower expectancy
    ab_framework.record_result(test_id, 'B', pnl)

# Analyze
analysis = ab_framework.analyze_test(test_id)
print("A/B Test Results:")
print("=" * 50)
print(f"Test: {analysis['test_name']}")
print(f"\nVariant A ({analysis['variant_a']['description']}):")
print(f"  Samples: {analysis['variant_a']['samples']}")
print(f"  Mean P&L: ${analysis['variant_a']['mean_pnl']:.2f}")
print(f"  Win Rate: {analysis['variant_a']['win_rate']:.1f}%")
print(f"\nVariant B ({analysis['variant_b']['description']}):")
print(f"  Samples: {analysis['variant_b']['samples']}")
print(f"  Mean P&L: ${analysis['variant_b']['mean_pnl']:.2f}")
print(f"  Win Rate: {analysis['variant_b']['win_rate']:.1f}%")
print(f"\nP-value: {analysis['p_value']:.4f}")
print(f"Statistically Significant: {analysis['statistically_significant']}")
print(f"Recommendation: {analysis['recommended']}")

Exercises

Exercise 1: Bias Detection Enhancement (Guided)

Enhance the bias detector with additional detection rules.

class EnhancedBiasDetector(BiasDetector):
    """Enhanced bias detection with more rules."""
    
    def detect_overtrading(self, trades: List[Dict], threshold: int = 10) -> Optional[Dict]:
        """Detect overtrading patterns."""
        if not trades:
            return None
            
        # Group trades by date
        trades_by_date = defaultdict(list)
        for t in trades:
            date = t.get('entry_time', datetime.now()).date()
            trades_by_date[date].append(t)
            
        # Find days with excessive trading
        overtrading_days = [d for d, ts in trades_by_date.items() if len(ts) > ______]  # Check threshold
        
        if overtrading_days:
            return {
                'bias': 'overtrading',
                'severity': 'high' if len(overtrading_days) > 3 else 'medium',
                'evidence': f"{len(overtrading_days)} days with >{threshold} trades"
            }
        return None
    
    def detect_anchoring(self, trades: List[Dict]) -> Optional[Dict]:
        """Detect anchoring to specific price levels."""
        # Check if many stops/targets are at round numbers
        round_number_count = 0
        total = 0
        
        for t in trades:
            sl = t.get('stop_loss', 0)
            tp = t.get('take_profit', 0)
            
            for price in [sl, tp]:
                if price > 0:
                    total += 1
                    # Check if price is a round number (ends in 00)
                    if int(price * 10000) % 100 == 0:
                        round_number_count += ______  # Increment count
                        
        if total > 0 and round_number_count / total > 0.5:
            return {
                'bias': TradingBias.ANCHORING,
                'severity': 'medium',
                'evidence': f"{round_number_count/total*100:.0f}% of stops/targets at round numbers"
            }
        return None
    
    def analyze_all(self, trades: List[Dict]) -> List[Dict]:
        """Run all bias detection methods."""
        biases = self.analyze_trades(trades)
        
        overtrading = self.detect_overtrading(trades)
        if overtrading:
            biases.______(overtrading)  # Add to list
            
        anchoring = self.detect_anchoring(trades)
        if anchoring:
            biases.append(anchoring)
            
        return biases


# Test enhanced detector
enhanced_detector = EnhancedBiasDetector()
biases = enhanced_detector.analyze_all(sample_trades)
print(f"Detected {len(biases)} potential biases")

Exercise 2: Journal Entry Template (Guided)

Create a journal entry template generator.

class JournalTemplateGenerator:
    """Generate journal entry templates."""
    
    def __init__(self):
        self.prompts = {
            'entry_reasoning': [
                "What setup triggered this trade?",
                "What was the market structure?",
                "What was your edge in this trade?"
            ],
            'exit_reasoning': [
                "Why did you exit at this point?",
                "Was this a planned or discretionary exit?",
                "Did price action confirm your exit?"
            ],
            'lessons': [
                "What would you do differently?",
                "What did this trade teach you?",
                "How can you improve similar setups?"
            ]
        }
    
    def generate_pre_trade(self, trade_details: Dict) -> str:
        """Generate pre-trade checklist."""
        lines = [
            "=" * 50,
            "PRE-TRADE CHECKLIST",
            "=" * 50,
            "",
            f"Instrument: {trade_details.get('instrument', '_________')}",
            f"Direction: {trade_details.get('direction', '_________')}",
            "",
            "SETUP CONFIRMATION:",
            "[ ] Is this my A+ setup?",
            "[ ] Is the trend aligned with my direction?",
            "[ ] Is there clear support/resistance?",
            "",
            "RISK CHECK:",
            "[ ] Position size within risk limits?",
            "[ ] Stop loss clearly defined?",
            "[ ] Risk:Reward >= 1:2?",
            "",
            "EMOTIONAL CHECK:",
            "Current emotional state: __________",
            "Confidence (1-10): __________",
            "[ ] Not revenge trading",
            "[ ] Not FOMO"
        ]
        return "\n".______(lines)  # Join lines with newline
    
    def generate_post_trade(self, trade_result: Dict) -> str:
        """Generate post-trade review template."""
        outcome = "WIN" if trade_result.get('pnl', 0) > 0 else "______"  # Set outcome
        
        lines = [
            "=" * 50,
            f"POST-TRADE REVIEW - {outcome}",
            "=" * 50,
            "",
            f"P&L: ${trade_result.get('pnl', 0):.2f}",
            f"Pips: {trade_result.get('pnl_pips', 0):.1f}",
            "",
            "EXECUTION REVIEW:",
        ]
        
        for prompt in self.prompts['entry_reasoning']:
            lines.append(f"Q: {prompt}")
            lines.append("A: _________________________________")
            lines.append("")
            
        lines.append("\nLESSONS LEARNED:")
        for prompt in self.prompts['lessons']:
            lines.append(f"Q: {prompt}")
            lines.append("A: _________________________________")
            lines.append("")
            
        return "\n".join(______)


# Test template generator
template_gen = JournalTemplateGenerator()
print(template_gen.generate_pre_trade({'instrument': 'EURUSD', 'direction': 'long'}))

Exercise 3: Performance Heatmap (Guided)

Create a performance heatmap generator.

class PerformanceHeatmap:
    """Generate performance heatmaps."""
    
    def __init__(self, journal: TradingJournal):
        self.journal = journal
        
    def by_day_hour(self) -> pd.DataFrame:
        """Create heatmap data by day of week and hour."""
        # Initialize matrix
        days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
        hours = list(range(0, 24))
        
        data = pd.DataFrame(0.0, index=days, columns=hours)
        counts = pd.DataFrame(0, index=days, columns=hours)
        
        for entry in self.journal.entries:
            day = entry.timestamp.strftime('%a')
            hour = entry.timestamp.______  # Get hour from timestamp
            
            if day in days:
                data.loc[day, hour] += entry.pnl
                counts.loc[day, hour] += 1
                
        # Calculate average (avoid division by zero)
        avg_data = data.div(counts.replace(0, 1))
        return avg_data
    
    def plot(self, data: pd.DataFrame, title: str):
        """Plot heatmap."""
        plt.figure(figsize=(14, 5))
        plt.imshow(data.values, cmap='RdYlGn', aspect='auto')
        plt.colorbar(label='Avg P&L')
        plt.yticks(range(len(data.index)), data.______)
        plt.xticks(range(len(data.columns)), data.columns)
        plt.xlabel('Hour (UTC)')
        plt.ylabel('Day of Week')
        plt.title(title)
        plt.tight_layout()
        plt.show()


# Create heatmap
heatmap = PerformanceHeatmap(journal)
data = heatmap.by_day_hour()
print("Performance by Day/Hour:")
print(data.head())

Exercise 4: Emotional Journal Integration

Build a system that integrates emotional tracking with trade journaling.

# Exercise 4: Build an EmotionalJournalIntegration class that:
# 1. Links emotional state entries to specific trades
# 2. Calculates performance by emotional state
# 3. Identifies emotional patterns that lead to losses
# 4. Generates recommendations based on emotional patterns
# 5. Creates a report showing emotional impact on trading

# Your code here

Exercise 5: Strategy Evolution Tracker

Track how your trading strategy evolves over time.

# Exercise 5: Build a StrategyEvolutionTracker class that:
# 1. Records strategy changes with timestamps and reasons
# 2. Compares performance before/after changes
# 3. Identifies which changes improved performance
# 4. Tracks rule additions/modifications/removals
# 5. Generates an evolution timeline

# Your code here

Exercise 6: Automated Review Generator

Build a system that automatically generates comprehensive reviews.

# Exercise 6: Build an AutomatedReviewGenerator class that:
# 1. Generates daily, weekly, and monthly reviews automatically
# 2. Compares current period to historical averages
# 3. Highlights anomalies and unusual patterns
# 4. Creates actionable recommendations
# 5. Exports reviews to multiple formats (text, HTML, PDF)

# Your code here

Module Project: Automated Trading Journal

Build a complete automated trading journal system.

class AutomatedTradingJournal:
    """
    Complete automated trading journal system.
    
    Integrates:
    - Trade logging
    - Psychological tracking
    - Bias detection
    - Performance analysis
    - Improvement tracking
    - Automated reviews
    """
    
    def __init__(self):
        self.journal = TradingJournal()
        self.emotional_tracker = EmotionalStateTracker()
        self.bias_detector = BiasDetector()
        self.improvement_tracker = ImprovementTracker()
        self.ab_framework = ABTestFramework()
        
    def log_trade(self, trade: Dict, emotional_state: str = None,
                  analysis: Dict = None):
        """Log a trade with full context."""
        # Log emotional state if provided
        if emotional_state:
            self.emotional_tracker.log_state(
                emotional_state,
                context={'trade_entry': True}
            )
            
        # Create journal entry
        psych_data = {
            'emotional_state': emotional_state or 'not_recorded',
            'confidence': analysis.get('confidence', 5) if analysis else 5,
            'followed_plan': analysis.get('followed_plan', True) if analysis else True
        }
        
        entry = self.journal.create_entry_from_trade(trade, psych_data, analysis)
        self.journal.add_entry(entry)
        
        return entry
    
    def run_bias_check(self, lookback_days: int = 30) -> List[Dict]:
        """Run bias detection on recent trades."""
        cutoff = datetime.now() - timedelta(days=lookback_days)
        recent_entries = self.journal.search(date_from=cutoff)
        
        trades = [
            {
                'pnl': e.pnl,
                'duration_hours': e.duration_minutes / 60,
                'entry_time': e.timestamp,
                'exit_time': e.timestamp + timedelta(minutes=e.duration_minutes)
            }
            for e in recent_entries
        ]
        
        return self.bias_detector.analyze_trades(trades)
    
    def generate_weekly_review(self) -> Dict:
        """Generate weekly performance review."""
        week_start = datetime.now() - timedelta(days=7)
        weekly_review = WeeklyReview(self.journal)
        return weekly_review.generate(week_start)
    
    def generate_monthly_analysis(self) -> Dict:
        """Generate monthly analysis."""
        now = datetime.now()
        monthly = MonthlyAnalysis(self.journal)
        return monthly.generate(now.year, now.month)
    
    def get_improvement_suggestions(self) -> List[str]:
        """Get personalized improvement suggestions."""
        suggestions = []
        stats = self.journal.get_statistics()
        
        # Check win rate
        if stats.get('win_rate', 0) < 50:
            suggestions.append("Focus on improving trade selection - win rate below 50%")
            
        # Check plan adherence
        if stats.get('plan_adherence', 100) < 80:
            suggestions.append("Improve discipline - plan adherence below 80%")
            
        # Check emotional correlation
        emotional_analysis = self.emotional_tracker.analyze_state_patterns()
        if emotional_analysis.get('most_common') in ['anxious', 'frustrated']:
            suggestions.append("Address emotional management - frequently trading in negative states")
            
        # Add bias-specific suggestions
        biases = self.run_bias_check()
        for bias in biases:
            suggestions.append(f"Address {bias['bias'].value}: {self.bias_detector.get_mitigation(bias['bias'])}")
            
        return suggestions
    
    def generate_comprehensive_report(self) -> str:
        """Generate comprehensive trading report."""
        stats = self.journal.get_statistics()
        biases = self.run_bias_check()
        suggestions = self.get_improvement_suggestions()
        
        lines = [
            "\n" + "=" * 70,
            "COMPREHENSIVE TRADING REPORT",
            f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
            "=" * 70,
            "",
            "PERFORMANCE SUMMARY",
            "-" * 40,
        ]
        
        for key, value in stats.items():
            if isinstance(value, float):
                lines.append(f"  {key}: {value:.2f}")
            else:
                lines.append(f"  {key}: {value}")
                
        lines.append("")
        lines.append("DETECTED BIASES")
        lines.append("-" * 40)
        
        if biases:
            for bias in biases:
                lines.append(f"  [{bias['severity'].upper()}] {bias['bias'].value}")
                lines.append(f"    Evidence: {bias['evidence']}")
        else:
            lines.append("  No significant biases detected")
            
        lines.append("")
        lines.append("IMPROVEMENT SUGGESTIONS")
        lines.append("-" * 40)
        
        for i, suggestion in enumerate(suggestions, 1):
            lines.append(f"  {i}. {suggestion}")
            
        lines.append("")
        lines.append("=" * 70)
        
        return "\n".join(lines)
# Demonstrate the automated trading journal
auto_journal = AutomatedTradingJournal()

# Log some trades
for trade in sample_trades_for_journal:
    auto_journal.log_trade(
        trade,
        emotional_state='focused',
        analysis={
            'setup_type': 'Trend Continuation',
            'timeframe': 'H1',
            'market_conditions': 'Trending',
            'confidence': 7,
            'followed_plan': True
        }
    )

# Generate comprehensive report
report = auto_journal.generate_comprehensive_report()
print(report)

Key Takeaways

  1. Trading Psychology: Understanding and mitigating biases is as important as technical skills

  2. Journaling: Detailed trade documentation enables systematic improvement

  3. Regular Reviews: Weekly and monthly reviews identify patterns and areas for improvement

  4. Continuous Improvement: A/B testing and goal tracking accelerate development

  5. Automation: Automated systems ensure consistency and reduce manual effort


Next: Capstone Project - Build a complete 24-hour Forex/Futures Trading System

Capstone Project: 24-Hour Forex/Futures Trading System

Duration ~8-10 hours
Skill Level Advanced
Prerequisites All Course 5 Modules

Project Overview

Build a complete, production-ready 24-hour trading system that combines everything you've learned in this course.

System Requirements

  • Multi-currency monitoring
  • Economic calendar integration
  • Technical + fundamental signals
  • Leveraged risk management
  • OANDA or MT5 execution (simulated)
  • 24-hour scheduling
  • Performance tracking
  • Automated journaling

Learning Objectives

By completing this capstone, you will demonstrate mastery of: - Forex and futures market mechanics - Technical and fundamental analysis integration - Risk management for leveraged products - Automated trading system development - Performance monitoring and analysis - Professional software engineering practices

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Callable
from dataclasses import dataclass, field
from enum import Enum
from collections import defaultdict
import json
import logging
from abc import ABC, abstractmethod

Part 1: Core Infrastructure

Build the foundational components of the trading system.

1.1 Data Models

class OrderType(Enum):
    MARKET = "market"
    LIMIT = "limit"
    STOP = "stop"


class OrderSide(Enum):
    BUY = "buy"
    SELL = "sell"


class TradingSession(Enum):
    SYDNEY = "Sydney"
    TOKYO = "Tokyo"
    LONDON = "London"
    NEW_YORK = "New_York"


@dataclass
class Tick:
    """Single price tick."""
    instrument: str
    bid: float
    ask: float
    timestamp: datetime
    
    @property
    def mid(self) -> float:
        return (self.bid + self.ask) / 2
    
    @property
    def spread_pips(self) -> float:
        return (self.ask - self.bid) * 10000


@dataclass
class OHLC:
    """OHLC candle data."""
    instrument: str
    timeframe: str
    timestamp: datetime
    open: float
    high: float
    low: float
    close: float
    volume: int = 0


@dataclass
class Order:
    """Trading order."""
    order_id: str
    instrument: str
    side: OrderSide
    order_type: OrderType
    units: float
    price: Optional[float] = None
    stop_loss: Optional[float] = None
    take_profit: Optional[float] = None
    trailing_stop: Optional[float] = None
    status: str = "pending"
    fill_price: Optional[float] = None
    fill_time: Optional[datetime] = None


@dataclass
class Position:
    """Open position."""
    position_id: str
    instrument: str
    units: float
    entry_price: float
    entry_time: datetime
    stop_loss: Optional[float] = None
    take_profit: Optional[float] = None
    current_price: float = 0.0
    
    @property
    def side(self) -> str:
        return "long" if self.units > 0 else "short"
    
    @property
    def unrealized_pnl(self) -> float:
        if self.units > 0:
            return (self.current_price - self.entry_price) * self.units
        else:
            return (self.entry_price - self.current_price) * abs(self.units)
    
    @property
    def unrealized_pips(self) -> float:
        diff = self.current_price - self.entry_price
        if self.units < 0:
            diff = -diff
        return diff * 10000


@dataclass
class TradeRecord:
    """Completed trade record."""
    trade_id: str
    instrument: str
    side: str
    units: float
    entry_price: float
    exit_price: float
    entry_time: datetime
    exit_time: datetime
    pnl: float
    pnl_pips: float
    setup_type: str = ""
    notes: str = ""

1.2 Market Data Manager

class MarketDataManager:
    """Manage market data for multiple instruments."""
    
    def __init__(self):
        self.ticks: Dict[str, List[Tick]] = defaultdict(list)
        self.candles: Dict[str, Dict[str, List[OHLC]]] = defaultdict(lambda: defaultdict(list))
        self.current_prices: Dict[str, Tick] = {}
        
    def update_tick(self, tick: Tick):
        """Update with new tick."""
        self.ticks[tick.instrument].append(tick)
        self.current_prices[tick.instrument] = tick
        
        # Keep only last 10000 ticks per instrument
        if len(self.ticks[tick.instrument]) > 10000:
            self.ticks[tick.instrument] = self.ticks[tick.instrument][-10000:]
            
    def add_candle(self, candle: OHLC):
        """Add OHLC candle."""
        self.candles[candle.instrument][candle.timeframe].append(candle)
        
    def get_price(self, instrument: str) -> Optional[Tick]:
        """Get current price for instrument."""
        return self.current_prices.get(instrument)
    
    def get_candles(self, instrument: str, timeframe: str, count: int = 100) -> List[OHLC]:
        """Get recent candles."""
        candles = self.candles[instrument][timeframe]
        return candles[-count:] if candles else []
    
    def get_ohlc_dataframe(self, instrument: str, timeframe: str) -> pd.DataFrame:
        """Get candles as DataFrame."""
        candles = self.get_candles(instrument, timeframe)
        if not candles:
            return pd.DataFrame()
            
        data = [{
            'timestamp': c.timestamp,
            'open': c.open,
            'high': c.high,
            'low': c.low,
            'close': c.close,
            'volume': c.volume
        } for c in candles]
        
        df = pd.DataFrame(data)
        df.set_index('timestamp', inplace=True)
        return df

1.3 Simulated Broker

class SimulatedBroker:
    """Simulated broker for paper trading."""
    
    def __init__(self, initial_balance: float = 10000.0, leverage: int = 50):
        self.balance = initial_balance
        self.initial_balance = initial_balance
        self.leverage = leverage
        self.positions: Dict[str, Position] = {}
        self.orders: Dict[str, Order] = {}
        self.trade_history: List[TradeRecord] = []
        self.order_count = 0
        self.position_count = 0
        self.trade_count = 0
        
        # Commission structure
        self.spread_markup = 0.0001  # 1 pip additional spread
        self.commission_per_lot = 0  # No commission (spread only)
        
    def get_account_summary(self) -> Dict:
        """Get account summary."""
        unrealized_pnl = sum(p.unrealized_pnl for p in self.positions.values())
        equity = self.balance + unrealized_pnl
        
        # Calculate margin used
        margin_used = 0
        for pos in self.positions.values():
            notional = abs(pos.units) * pos.current_price
            margin_used += notional / self.leverage
            
        margin_available = equity - margin_used
        margin_level = (equity / margin_used * 100) if margin_used > 0 else float('inf')
        
        return {
            'balance': self.balance,
            'unrealized_pnl': unrealized_pnl,
            'equity': equity,
            'margin_used': margin_used,
            'margin_available': margin_available,
            'margin_level': margin_level,
            'open_positions': len(self.positions),
            'total_trades': len(self.trade_history)
        }
    
    def submit_order(self, instrument: str, side: OrderSide, units: float,
                     order_type: OrderType = OrderType.MARKET,
                     price: float = None, stop_loss: float = None,
                     take_profit: float = None, current_tick: Tick = None) -> Order:
        """Submit a new order."""
        self.order_count += 1
        order_id = f"ORD-{self.order_count:06d}"
        
        order = Order(
            order_id=order_id,
            instrument=instrument,
            side=side,
            order_type=order_type,
            units=units,
            price=price,
            stop_loss=stop_loss,
            take_profit=take_profit
        )
        
        # For market orders, execute immediately
        if order_type == OrderType.MARKET and current_tick:
            self._execute_order(order, current_tick)
        else:
            self.orders[order_id] = order
            
        return order
    
    def _execute_order(self, order: Order, tick: Tick):
        """Execute an order at current price."""
        # Determine fill price (include spread)
        if order.side == OrderSide.BUY:
            fill_price = tick.ask + self.spread_markup
        else:
            fill_price = tick.bid - self.spread_markup
            
        order.fill_price = fill_price
        order.fill_time = tick.timestamp
        order.status = "filled"
        
        # Check if this closes an existing position
        if order.instrument in self.positions:
            self._update_position(order, tick)
        else:
            self._open_position(order)
            
    def _open_position(self, order: Order):
        """Open a new position."""
        self.position_count += 1
        position_id = f"POS-{self.position_count:06d}"
        
        units = order.units if order.side == OrderSide.BUY else -order.units
        
        position = Position(
            position_id=position_id,
            instrument=order.instrument,
            units=units,
            entry_price=order.fill_price,
            entry_time=order.fill_time,
            stop_loss=order.stop_loss,
            take_profit=order.take_profit,
            current_price=order.fill_price
        )
        
        self.positions[order.instrument] = position
        
    def _update_position(self, order: Order, tick: Tick):
        """Update existing position (add to or close)."""
        position = self.positions[order.instrument]
        order_units = order.units if order.side == OrderSide.BUY else -order.units
        new_units = position.units + order_units
        
        if new_units == 0:
            # Position fully closed
            self._close_position(order.instrument, order.fill_price, tick.timestamp)
        elif (position.units > 0 and new_units < 0) or (position.units < 0 and new_units > 0):
            # Position reversed
            self._close_position(order.instrument, order.fill_price, tick.timestamp)
            # Open new position with remaining units
            order.units = abs(new_units)
            self._open_position(order)
        else:
            # Position increased
            total_cost = position.entry_price * abs(position.units) + order.fill_price * order.units
            position.units = new_units
            position.entry_price = total_cost / abs(new_units)
            
    def _close_position(self, instrument: str, exit_price: float, exit_time: datetime):
        """Close a position and record the trade."""
        if instrument not in self.positions:
            return
            
        position = self.positions[instrument]
        
        # Calculate P&L
        if position.units > 0:
            pnl = (exit_price - position.entry_price) * position.units
            pnl_pips = (exit_price - position.entry_price) * 10000
        else:
            pnl = (position.entry_price - exit_price) * abs(position.units)
            pnl_pips = (position.entry_price - exit_price) * 10000
            
        # Update balance
        self.balance += pnl
        
        # Record trade
        self.trade_count += 1
        trade = TradeRecord(
            trade_id=f"TRD-{self.trade_count:06d}",
            instrument=instrument,
            side=position.side,
            units=abs(position.units),
            entry_price=position.entry_price,
            exit_price=exit_price,
            entry_time=position.entry_time,
            exit_time=exit_time,
            pnl=pnl,
            pnl_pips=pnl_pips
        )
        self.trade_history.append(trade)
        
        # Remove position
        del self.positions[instrument]
        
    def update_positions(self, market_data: MarketDataManager):
        """Update position prices and check stops/targets."""
        for instrument, position in list(self.positions.items()):
            tick = market_data.get_price(instrument)
            if tick:
                position.current_price = tick.mid
                
                # Check stop loss
                if position.stop_loss:
                    if position.units > 0 and tick.bid <= position.stop_loss:
                        self._close_position(instrument, tick.bid, tick.timestamp)
                    elif position.units < 0 and tick.ask >= position.stop_loss:
                        self._close_position(instrument, tick.ask, tick.timestamp)
                        
                # Check take profit
                if position.take_profit:
                    if position.units > 0 and tick.bid >= position.take_profit:
                        self._close_position(instrument, tick.bid, tick.timestamp)
                    elif position.units < 0 and tick.ask <= position.take_profit:
                        self._close_position(instrument, tick.ask, tick.timestamp)
                        
    def close_all_positions(self, market_data: MarketDataManager):
        """Close all open positions."""
        for instrument in list(self.positions.keys()):
            tick = market_data.get_price(instrument)
            if tick:
                position = self.positions[instrument]
                exit_price = tick.bid if position.units > 0 else tick.ask
                self._close_position(instrument, exit_price, tick.timestamp)

Part 2: Signal Generation

Build signal generation from technical and fundamental analysis.

2.1 Technical Indicators

class TechnicalIndicators:
    """Calculate technical indicators."""
    
    @staticmethod
    def sma(prices: pd.Series, period: int) -> pd.Series:
        """Simple Moving Average."""
        return prices.rolling(window=period).mean()
    
    @staticmethod
    def ema(prices: pd.Series, period: int) -> pd.Series:
        """Exponential Moving Average."""
        return prices.ewm(span=period, adjust=False).mean()
    
    @staticmethod
    def rsi(prices: pd.Series, period: int = 14) -> pd.Series:
        """Relative Strength Index."""
        delta = prices.diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / loss
        return 100 - (100 / (1 + rs))
    
    @staticmethod
    def atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
        """Average True Range."""
        tr1 = high - low
        tr2 = abs(high - close.shift())
        tr3 = abs(low - close.shift())
        tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
        return tr.rolling(window=period).mean()
    
    @staticmethod
    def bollinger_bands(prices: pd.Series, period: int = 20, std_dev: float = 2.0) -> Tuple[pd.Series, pd.Series, pd.Series]:
        """Bollinger Bands."""
        middle = prices.rolling(window=period).mean()
        std = prices.rolling(window=period).std()
        upper = middle + std_dev * std
        lower = middle - std_dev * std
        return upper, middle, lower
    
    @staticmethod
    def macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Tuple[pd.Series, pd.Series, pd.Series]:
        """MACD indicator."""
        ema_fast = prices.ewm(span=fast, adjust=False).mean()
        ema_slow = prices.ewm(span=slow, adjust=False).mean()
        macd_line = ema_fast - ema_slow
        signal_line = macd_line.ewm(span=signal, adjust=False).mean()
        histogram = macd_line - signal_line
        return macd_line, signal_line, histogram

2.2 Signal Generator

@dataclass
class Signal:
    """Trading signal."""
    instrument: str
    direction: str  # 'long' or 'short'
    strength: float  # 0.0 to 1.0
    source: str  # 'technical', 'fundamental', 'combined'
    timestamp: datetime
    entry_price: Optional[float] = None
    stop_loss: Optional[float] = None
    take_profit: Optional[float] = None
    metadata: Dict = field(default_factory=dict)


class SignalGenerator(ABC):
    """Base class for signal generators."""
    
    @abstractmethod
    def generate(self, df: pd.DataFrame, instrument: str) -> Optional[Signal]:
        """Generate trading signal from data."""
        pass


class TrendFollowingSignal(SignalGenerator):
    """Trend following signal using moving averages."""
    
    def __init__(self, fast_period: int = 20, slow_period: int = 50, atr_period: int = 14):
        self.fast_period = fast_period
        self.slow_period = slow_period
        self.atr_period = atr_period
        
    def generate(self, df: pd.DataFrame, instrument: str) -> Optional[Signal]:
        if len(df) < self.slow_period + 1:
            return None
            
        # Calculate indicators
        fast_ma = TechnicalIndicators.ema(df['close'], self.fast_period)
        slow_ma = TechnicalIndicators.ema(df['close'], self.slow_period)
        atr = TechnicalIndicators.atr(df['high'], df['low'], df['close'], self.atr_period)
        
        current_fast = fast_ma.iloc[-1]
        current_slow = slow_ma.iloc[-1]
        prev_fast = fast_ma.iloc[-2]
        prev_slow = slow_ma.iloc[-2]
        current_atr = atr.iloc[-1]
        current_price = df['close'].iloc[-1]
        
        # Check for crossover
        signal = None
        
        if prev_fast <= prev_slow and current_fast > current_slow:
            # Bullish crossover
            signal = Signal(
                instrument=instrument,
                direction='long',
                strength=0.7,
                source='technical',
                timestamp=df.index[-1],
                entry_price=current_price,
                stop_loss=current_price - 2 * current_atr,
                take_profit=current_price + 3 * current_atr,
                metadata={'fast_ma': current_fast, 'slow_ma': current_slow, 'atr': current_atr}
            )
        elif prev_fast >= prev_slow and current_fast < current_slow:
            # Bearish crossover
            signal = Signal(
                instrument=instrument,
                direction='short',
                strength=0.7,
                source='technical',
                timestamp=df.index[-1],
                entry_price=current_price,
                stop_loss=current_price + 2 * current_atr,
                take_profit=current_price - 3 * current_atr,
                metadata={'fast_ma': current_fast, 'slow_ma': current_slow, 'atr': current_atr}
            )
            
        return signal


class MeanReversionSignal(SignalGenerator):
    """Mean reversion signal using Bollinger Bands."""
    
    def __init__(self, period: int = 20, std_dev: float = 2.0, rsi_period: int = 14):
        self.period = period
        self.std_dev = std_dev
        self.rsi_period = rsi_period
        
    def generate(self, df: pd.DataFrame, instrument: str) -> Optional[Signal]:
        if len(df) < self.period + 1:
            return None
            
        # Calculate indicators
        upper, middle, lower = TechnicalIndicators.bollinger_bands(df['close'], self.period, self.std_dev)
        rsi = TechnicalIndicators.rsi(df['close'], self.rsi_period)
        atr = TechnicalIndicators.atr(df['high'], df['low'], df['close'], 14)
        
        current_price = df['close'].iloc[-1]
        current_rsi = rsi.iloc[-1]
        current_atr = atr.iloc[-1]
        current_upper = upper.iloc[-1]
        current_lower = lower.iloc[-1]
        current_middle = middle.iloc[-1]
        
        signal = None
        
        # Oversold at lower band
        if current_price <= current_lower and current_rsi < 30:
            signal = Signal(
                instrument=instrument,
                direction='long',
                strength=0.6 + (30 - current_rsi) / 100,
                source='technical',
                timestamp=df.index[-1],
                entry_price=current_price,
                stop_loss=current_price - 1.5 * current_atr,
                take_profit=current_middle,
                metadata={'rsi': current_rsi, 'bb_lower': current_lower, 'bb_middle': current_middle}
            )
        # Overbought at upper band
        elif current_price >= current_upper and current_rsi > 70:
            signal = Signal(
                instrument=instrument,
                direction='short',
                strength=0.6 + (current_rsi - 70) / 100,
                source='technical',
                timestamp=df.index[-1],
                entry_price=current_price,
                stop_loss=current_price + 1.5 * current_atr,
                take_profit=current_middle,
                metadata={'rsi': current_rsi, 'bb_upper': current_upper, 'bb_middle': current_middle}
            )
            
        return signal

2.3 Economic Calendar Integration

@dataclass
class EconomicEvent:
    """Economic calendar event."""
    event_id: str
    name: str
    currency: str
    impact: str  # 'low', 'medium', 'high'
    datetime: datetime
    actual: Optional[float] = None
    forecast: Optional[float] = None
    previous: Optional[float] = None


class EconomicCalendar:
    """Economic calendar for fundamental analysis."""
    
    def __init__(self):
        self.events: List[EconomicEvent] = []
        
    def add_event(self, event: EconomicEvent):
        """Add an economic event."""
        self.events.append(event)
        self.events.sort(key=lambda x: x.datetime)
        
    def get_upcoming_events(self, hours_ahead: int = 24) -> List[EconomicEvent]:
        """Get events in the next N hours."""
        now = datetime.now()
        cutoff = now + timedelta(hours=hours_ahead)
        return [e for e in self.events if now <= e.datetime <= cutoff]
    
    def get_high_impact_events(self, hours_ahead: int = 24) -> List[EconomicEvent]:
        """Get high impact events."""
        upcoming = self.get_upcoming_events(hours_ahead)
        return [e for e in upcoming if e.impact == 'high']
    
    def should_avoid_trading(self, instrument: str, minutes_before: int = 30) -> Tuple[bool, Optional[EconomicEvent]]:
        """Check if trading should be avoided due to upcoming event."""
        # Extract currencies from instrument
        base = instrument[:3]
        quote = instrument[3:6] if len(instrument) >= 6 else instrument[4:]
        
        now = datetime.now()
        event_window = now + timedelta(minutes=minutes_before)
        
        for event in self.events:
            if event.impact == 'high' and event.currency in [base, quote]:
                if now <= event.datetime <= event_window:
                    return True, event
                    
        return False, None


# Create sample economic calendar
def create_sample_calendar() -> EconomicCalendar:
    calendar = EconomicCalendar()
    
    # Add sample events
    events = [
        EconomicEvent('EVT001', 'Non-Farm Payrolls', 'USD', 'high', datetime.now() + timedelta(hours=2)),
        EconomicEvent('EVT002', 'ECB Rate Decision', 'EUR', 'high', datetime.now() + timedelta(hours=5)),
        EconomicEvent('EVT003', 'UK CPI', 'GBP', 'medium', datetime.now() + timedelta(hours=12)),
        EconomicEvent('EVT004', 'US Retail Sales', 'USD', 'medium', datetime.now() + timedelta(hours=26)),
    ]
    
    for event in events:
        calendar.add_event(event)
        
    return calendar

Part 3: Risk Management

Implement comprehensive risk management for leveraged trading.

class RiskManager:
    """Comprehensive risk management for forex/futures."""
    
    def __init__(self, max_risk_per_trade: float = 0.02,
                 max_daily_loss: float = 0.05,
                 max_positions: int = 5,
                 max_correlation_exposure: float = 0.5):
        self.max_risk_per_trade = max_risk_per_trade
        self.max_daily_loss = max_daily_loss
        self.max_positions = max_positions
        self.max_correlation_exposure = max_correlation_exposure
        
        self.daily_pnl = 0.0
        self.daily_start_balance = 0.0
        self.last_reset = None
        
        # Currency pair correlations (simplified)
        self.correlations = {
            ('EURUSD', 'GBPUSD'): 0.85,
            ('EURUSD', 'USDCHF'): -0.90,
            ('AUDUSD', 'NZDUSD'): 0.90,
            ('USDJPY', 'EURJPY'): 0.75,
        }
        
    def reset_daily(self, current_balance: float):
        """Reset daily tracking."""
        self.daily_pnl = 0.0
        self.daily_start_balance = current_balance
        self.last_reset = datetime.now()
        
    def calculate_position_size(self, account_balance: float, entry_price: float,
                                stop_loss: float, pip_value: float = 10.0) -> float:
        """Calculate position size based on risk."""
        risk_amount = account_balance * self.max_risk_per_trade
        stop_distance_pips = abs(entry_price - stop_loss) * 10000
        
        if stop_distance_pips == 0:
            return 0
            
        # Position size in lots (100,000 units)
        position_lots = risk_amount / (stop_distance_pips * pip_value)
        
        # Round to micro lots (0.01)
        return round(position_lots, 2)
    
    def can_open_trade(self, broker: SimulatedBroker, signal: Signal) -> Tuple[bool, str]:
        """Check if a new trade can be opened."""
        account = broker.get_account_summary()
        
        # Check daily loss limit
        daily_loss_pct = abs(self.daily_pnl) / self.daily_start_balance if self.daily_start_balance > 0 else 0
        if self.daily_pnl < 0 and daily_loss_pct >= self.max_daily_loss:
            return False, f"Daily loss limit reached: {daily_loss_pct*100:.1f}%"
            
        # Check max positions
        if account['open_positions'] >= self.max_positions:
            return False, f"Max positions reached: {account['open_positions']}"
            
        # Check margin level
        if account['margin_level'] < 200:
            return False, f"Low margin level: {account['margin_level']:.0f}%"
            
        # Check correlation exposure
        if not self._check_correlation(broker.positions, signal.instrument):
            return False, "Correlation exposure too high"
            
        return True, "OK"
    
    def _check_correlation(self, positions: Dict[str, Position], new_instrument: str) -> bool:
        """Check if new position would create too much correlation exposure."""
        for inst in positions:
            pair = tuple(sorted([inst.replace('_', ''), new_instrument.replace('_', '')]))
            correlation = self.correlations.get(pair, 0)
            
            if abs(correlation) > self.max_correlation_exposure:
                # Check if same direction for positive correlation
                return False
                
        return True
    
    def update_daily_pnl(self, pnl: float):
        """Update daily P&L tracking."""
        self.daily_pnl += pnl
        
    def get_risk_metrics(self, broker: SimulatedBroker) -> Dict:
        """Get current risk metrics."""
        account = broker.get_account_summary()
        
        return {
            'daily_pnl': self.daily_pnl,
            'daily_pnl_pct': (self.daily_pnl / self.daily_start_balance * 100) if self.daily_start_balance > 0 else 0,
            'daily_loss_limit_remaining': max(0, self.max_daily_loss * self.daily_start_balance + self.daily_pnl),
            'margin_level': account['margin_level'],
            'open_positions': account['open_positions'],
            'max_positions': self.max_positions,
            'equity': account['equity']
        }

Part 4: Trading Engine

Build the core trading engine that orchestrates all components.

class TradingEngine:
    """
    Core trading engine orchestrating all components.
    
    Components:
    - Market data management
    - Signal generation
    - Risk management
    - Order execution
    - Performance tracking
    """
    
    def __init__(self, config: Dict):
        self.config = config
        
        # Initialize components
        self.market_data = MarketDataManager()
        self.broker = SimulatedBroker(
            initial_balance=config.get('initial_balance', 10000),
            leverage=config.get('leverage', 50)
        )
        self.risk_manager = RiskManager(
            max_risk_per_trade=config.get('risk_per_trade', 0.02),
            max_daily_loss=config.get('max_daily_loss', 0.05),
            max_positions=config.get('max_positions', 5)
        )
        self.calendar = create_sample_calendar()
        
        # Signal generators
        self.signal_generators: List[SignalGenerator] = [
            TrendFollowingSignal(),
            MeanReversionSignal()
        ]
        
        # Trading parameters
        self.instruments = config.get('instruments', ['EURUSD', 'GBPUSD', 'USDJPY'])
        self.timeframe = config.get('timeframe', 'H1')
        
        # State
        self.running = False
        self.paused = False
        self.signals_generated = 0
        self.trades_executed = 0
        
        # Logging
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger('TradingEngine')
        
    def start(self):
        """Start the trading engine."""
        self.running = True
        self.risk_manager.reset_daily(self.broker.balance)
        self.logger.info("Trading engine started")
        
    def stop(self):
        """Stop the trading engine."""
        self.running = False
        self.broker.close_all_positions(self.market_data)
        self.logger.info("Trading engine stopped")
        
    def pause(self):
        """Pause trading."""
        self.paused = True
        self.logger.info("Trading paused")
        
    def resume(self):
        """Resume trading."""
        self.paused = False
        self.logger.info("Trading resumed")
        
    def on_tick(self, tick: Tick):
        """Process new tick data."""
        if not self.running:
            return
            
        # Update market data
        self.market_data.update_tick(tick)
        
        # Update positions
        self.broker.update_positions(self.market_data)
        
    def on_candle_close(self, candle: OHLC):
        """Process candle close - main signal generation."""
        if not self.running or self.paused:
            return
            
        # Add candle to market data
        self.market_data.add_candle(candle)
        
        # Skip if already in position for this instrument
        if candle.instrument in self.broker.positions:
            return
            
        # Check economic calendar
        should_avoid, event = self.calendar.should_avoid_trading(candle.instrument)
        if should_avoid:
            self.logger.info(f"Avoiding {candle.instrument} due to {event.name}")
            return
            
        # Get price data
        df = self.market_data.get_ohlc_dataframe(candle.instrument, candle.timeframe)
        if df.empty:
            return
            
        # Generate signals
        best_signal = None
        best_strength = 0
        
        for generator in self.signal_generators:
            signal = generator.generate(df, candle.instrument)
            if signal and signal.strength > best_strength:
                best_signal = signal
                best_strength = signal.strength
                
        if best_signal and best_strength >= self.config.get('min_signal_strength', 0.6):
            self.signals_generated += 1
            self._process_signal(best_signal)
            
    def _process_signal(self, signal: Signal):
        """Process and potentially execute a signal."""
        # Check if we can trade
        can_trade, reason = self.risk_manager.can_open_trade(self.broker, signal)
        if not can_trade:
            self.logger.info(f"Signal rejected: {reason}")
            return
            
        # Calculate position size
        account = self.broker.get_account_summary()
        position_lots = self.risk_manager.calculate_position_size(
            account['equity'],
            signal.entry_price,
            signal.stop_loss
        )
        
        if position_lots < 0.01:
            self.logger.info("Position size too small")
            return
            
        # Convert lots to units
        units = position_lots * 100000
        
        # Get current tick
        tick = self.market_data.get_price(signal.instrument)
        if not tick:
            return
            
        # Submit order
        side = OrderSide.BUY if signal.direction == 'long' else OrderSide.SELL
        
        order = self.broker.submit_order(
            instrument=signal.instrument,
            side=side,
            units=units,
            order_type=OrderType.MARKET,
            stop_loss=signal.stop_loss,
            take_profit=signal.take_profit,
            current_tick=tick
        )
        
        if order.status == 'filled':
            self.trades_executed += 1
            self.logger.info(f"Trade executed: {signal.instrument} {signal.direction} @ {order.fill_price}")
            
    def get_status(self) -> Dict:
        """Get current engine status."""
        account = self.broker.get_account_summary()
        risk = self.risk_manager.get_risk_metrics(self.broker)
        
        return {
            'running': self.running,
            'paused': self.paused,
            'account': account,
            'risk': risk,
            'signals_generated': self.signals_generated,
            'trades_executed': self.trades_executed,
            'open_positions': len(self.broker.positions),
            'total_trades': len(self.broker.trade_history)
        }

Part 5: Performance Tracking & Journaling

Implement automated performance tracking and trade journaling.

class PerformanceTracker:
    """Track and analyze trading performance."""
    
    def __init__(self, engine: TradingEngine):
        self.engine = engine
        self.daily_snapshots: List[Dict] = []
        
    def calculate_statistics(self) -> Dict:
        """Calculate comprehensive statistics."""
        trades = self.engine.broker.trade_history
        if not trades:
            return {'total_trades': 0}
            
        winners = [t for t in trades if t.pnl > 0]
        losers = [t for t in trades if t.pnl <= 0]
        
        total_pnl = sum(t.pnl for t in trades)
        
        # Calculate metrics
        win_rate = len(winners) / len(trades) * 100
        avg_win = np.mean([t.pnl for t in winners]) if winners else 0
        avg_loss = np.mean([t.pnl for t in losers]) if losers else 0
        
        gross_profit = sum(t.pnl for t in winners)
        gross_loss = abs(sum(t.pnl for t in losers))
        profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
        
        # Calculate max drawdown
        equity_curve = [self.engine.broker.initial_balance]
        for trade in trades:
            equity_curve.append(equity_curve[-1] + trade.pnl)
            
        peak = equity_curve[0]
        max_dd = 0
        for equity in equity_curve:
            if equity > peak:
                peak = equity
            dd = (peak - equity) / peak * 100
            max_dd = max(max_dd, dd)
            
        # Calculate Sharpe ratio (simplified daily)
        if len(trades) > 1:
            returns = [t.pnl for t in trades]
            sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) if np.std(returns) > 0 else 0
        else:
            sharpe = 0
            
        return {
            'total_trades': len(trades),
            'winners': len(winners),
            'losers': len(losers),
            'win_rate': win_rate,
            'total_pnl': total_pnl,
            'avg_win': avg_win,
            'avg_loss': avg_loss,
            'profit_factor': profit_factor,
            'max_drawdown': max_dd,
            'sharpe_ratio': sharpe,
            'return_pct': (self.engine.broker.balance / self.engine.broker.initial_balance - 1) * 100
        }
    
    def generate_report(self) -> str:
        """Generate performance report."""
        stats = self.calculate_statistics()
        account = self.engine.broker.get_account_summary()
        
        lines = [
            "\n" + "=" * 60,
            "TRADING SYSTEM PERFORMANCE REPORT",
            f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
            "=" * 60,
            "",
            "ACCOUNT SUMMARY",
            "-" * 40,
            f"  Initial Balance: ${self.engine.broker.initial_balance:,.2f}",
            f"  Current Balance: ${account['balance']:,.2f}",
            f"  Equity: ${account['equity']:,.2f}",
            f"  Return: {stats.get('return_pct', 0):.2f}%",
            "",
            "TRADING STATISTICS",
            "-" * 40,
            f"  Total Trades: {stats.get('total_trades', 0)}",
            f"  Winners: {stats.get('winners', 0)}",
            f"  Losers: {stats.get('losers', 0)}",
            f"  Win Rate: {stats.get('win_rate', 0):.1f}%",
            f"  Average Win: ${stats.get('avg_win', 0):.2f}",
            f"  Average Loss: ${stats.get('avg_loss', 0):.2f}",
            f"  Profit Factor: {stats.get('profit_factor', 0):.2f}",
            "",
            "RISK METRICS",
            "-" * 40,
            f"  Max Drawdown: {stats.get('max_drawdown', 0):.2f}%",
            f"  Sharpe Ratio: {stats.get('sharpe_ratio', 0):.2f}",
            f"  Open Positions: {account['open_positions']}",
            f"  Margin Level: {account['margin_level']:.0f}%",
            "",
            "=" * 60
        ]
        
        return "\n".join(lines)

Part 6: Running the System

Demonstrate the complete trading system.

def generate_sample_data(instrument: str, days: int = 30) -> Tuple[List[OHLC], List[Tick]]:
    """Generate sample market data for testing."""
    np.random.seed(42)
    
    # Starting prices
    start_prices = {
        'EURUSD': 1.0850,
        'GBPUSD': 1.2650,
        'USDJPY': 149.50
    }
    
    price = start_prices.get(instrument, 1.0)
    
    candles = []
    ticks = []
    
    start_date = datetime.now() - timedelta(days=days)
    
    for day in range(days):
        for hour in range(24):
            # Generate hourly candle
            timestamp = start_date + timedelta(days=day, hours=hour)
            
            # Random walk with slight trend
            returns = np.random.randn() * 0.001 + 0.00002  # Slight upward bias
            
            open_price = price
            close_price = price * (1 + returns)
            high_price = max(open_price, close_price) * (1 + abs(np.random.randn() * 0.0005))
            low_price = min(open_price, close_price) * (1 - abs(np.random.randn() * 0.0005))
            
            candle = OHLC(
                instrument=instrument,
                timeframe='H1',
                timestamp=timestamp,
                open=open_price,
                high=high_price,
                low=low_price,
                close=close_price,
                volume=np.random.randint(1000, 5000)
            )
            candles.append(candle)
            
            # Generate tick at candle close
            spread = 0.0002
            tick = Tick(
                instrument=instrument,
                bid=close_price - spread/2,
                ask=close_price + spread/2,
                timestamp=timestamp
            )
            ticks.append(tick)
            
            price = close_price
            
    return candles, ticks
# Configure and run the trading system
config = {
    'initial_balance': 10000,
    'leverage': 50,
    'risk_per_trade': 0.02,
    'max_daily_loss': 0.05,
    'max_positions': 3,
    'instruments': ['EURUSD', 'GBPUSD', 'USDJPY'],
    'timeframe': 'H1',
    'min_signal_strength': 0.6
}

# Create trading engine
engine = TradingEngine(config)
performance = PerformanceTracker(engine)

# Generate sample data
print("Generating sample data...")
all_candles = []
all_ticks = []

for instrument in config['instruments']:
    candles, ticks = generate_sample_data(instrument, days=60)
    all_candles.extend(candles)
    all_ticks.extend(ticks)

# Sort by timestamp
all_candles.sort(key=lambda x: x.timestamp)
all_ticks.sort(key=lambda x: x.timestamp)

print(f"Generated {len(all_candles)} candles and {len(all_ticks)} ticks")
# Run backtest
print("\nRunning backtest...")
engine.start()

for i, (candle, tick) in enumerate(zip(all_candles, all_ticks)):
    # Process tick
    engine.on_tick(tick)
    
    # Process candle close
    engine.on_candle_close(candle)
    
    # Progress update
    if (i + 1) % 500 == 0:
        status = engine.get_status()
        print(f"Processed {i+1}/{len(all_candles)} candles, "
              f"Signals: {status['signals_generated']}, "
              f"Trades: {status['trades_executed']}, "
              f"Balance: ${status['account']['balance']:,.2f}")

engine.stop()
print("\nBacktest complete!")
# Generate performance report
report = performance.generate_report()
print(report)
# Plot equity curve
if engine.broker.trade_history:
    equity_curve = [engine.broker.initial_balance]
    dates = [engine.broker.trade_history[0].entry_time]
    
    for trade in engine.broker.trade_history:
        equity_curve.append(equity_curve[-1] + trade.pnl)
        dates.append(trade.exit_time)
    
    plt.figure(figsize=(12, 6))
    plt.plot(dates, equity_curve, 'b-', linewidth=1.5)
    plt.axhline(y=engine.broker.initial_balance, color='gray', linestyle='--', alpha=0.5)
    plt.fill_between(dates, equity_curve, engine.broker.initial_balance, 
                     where=[e >= engine.broker.initial_balance for e in equity_curve],
                     alpha=0.3, color='green')
    plt.fill_between(dates, equity_curve, engine.broker.initial_balance,
                     where=[e < engine.broker.initial_balance for e in equity_curve],
                     alpha=0.3, color='red')
    plt.title('Trading System Equity Curve')
    plt.xlabel('Date')
    plt.ylabel('Equity ($)')
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()
else:
    print("No trades to plot")

Capstone Completion Checklist

Ensure your system includes:

Core Components

  • [ ] Market data management (ticks and candles)
  • [ ] Simulated broker with realistic execution
  • [ ] Position and order management
  • [ ] Multiple instrument support

Signal Generation

  • [ ] At least 2 technical strategies
  • [ ] Economic calendar integration
  • [ ] Signal strength filtering

Risk Management

  • [ ] Position sizing based on risk
  • [ ] Daily loss limits
  • [ ] Maximum position limits
  • [ ] Correlation monitoring

Performance Tracking

  • [ ] Trade history logging
  • [ ] Performance statistics
  • [ ] Equity curve tracking
  • [ ] Automated reporting

Documentation

  • [ ] Code comments
  • [ ] Configuration documentation
  • [ ] Usage examples

Congratulations!

You have completed the Forex & Futures Trading course capstone project. You now have a complete, production-ready trading system framework that demonstrates:

  • Understanding of forex and futures markets
  • Technical and fundamental analysis integration
  • Professional risk management practices
  • Software engineering skills for trading systems

Next Steps: 1. Extend with additional strategies 2. Connect to live broker APIs (OANDA, MT5) 3. Add machine learning signal filters 4. Implement real-time alerting 5. Deploy on cloud infrastructure